Skip to content

Commit

Permalink
feat: add identity plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
j-lanson authored and mchernicoff committed Oct 28, 2024
1 parent cbb9504 commit 0d4312d
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 2 deletions.
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ members = [
"plugins/linguist",
"plugins/review",
"plugins/binary"
]
, "plugins/identity"]

# Make sure Hipcheck is run with `cargo run`.
#
Expand Down
2 changes: 1 addition & 1 deletion hipcheck/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ fn add_analysis(
};
let raw_policy = match analysis.policy_expression {
Some(x) => x,
None => core.default_policy_expr(publisher.0.clone(), plugin.0.clone())?.ok_or(hc_error!("plugin {}::{} does not have a default policy, please define a policy in your policy file"))?
None => core.default_policy_expr(publisher.0.clone(), plugin.0.clone())?.ok_or(hc_error!("plugin {}::{} does not have a default policy, please define a policy in your policy file", publisher.0, plugin.0))?
};
let analysis = Analysis {
publisher: publisher.0,
Expand Down
19 changes: 19 additions & 0 deletions plugins/identity/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "identity"
version = "0.1.0"
license = "Apache-2.0"
edition = "2021"
publish = false

[dependencies]
clap = { version = "4.5.20", features = ["derive"] }
hipcheck-sdk = { version = "0.1.0", path = "../../sdk/rust", features = ["macros"] }
log = "0.4.22"
schemars = "0.8.21"
serde = "1.0.213"
serde_json = "1.0.132"
tokio = { version = "1.41.0", features = ["rt"] }
toml = "0.8.19"

[dev-dependencies]
hipcheck-sdk = { path = "../../sdk/rust", features = ["mock_engine"] }
13 changes: 13 additions & 0 deletions plugins/identity/plugin.kdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
publisher "mitre"
name "identity"
version "0.1.0"
license "Apache-2.0"
entrypoint {
on arch="aarch64-apple-darwin" "./target/debug/identity"
on arch="x86_64-apple-darwin" "./target/debug/identity"
on arch="x86_64-unknown-linux-gnu" "./target/debug/identity"
on arch="x86_64-pc-windows-msvc" "./target/debug/identity"
}
dependencies {
plugin "mitre/git" version="0.1.0" manifest="./plugins/git/plugin.kdl"
}
238 changes: 238 additions & 0 deletions plugins/identity/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// SPDX-License-Identifier: Apache-2.0

use clap::Parser;
use hipcheck_sdk::{
prelude::*,
types::{LocalGitRepo, Target},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
fmt::{self, Display, Formatter},
result::Result as StdResult,
sync::OnceLock,
};

#[derive(Deserialize)]
struct Config {
#[serde(rename = "percent-threshold")]
percent_threshold: Option<u64>,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
pub struct Commit {
pub hash: String,
pub written_on: StdResult<String, String>,
pub committed_on: StdResult<String, String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Contributor {
pub name: String,
pub email: String,
}

impl Display for Contributor {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{} <{}>", self.name, self.email)
}
}

/// Temporary data structure for looking up the contributors of a commit
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct CommitContributorView {
pub commit: Commit,
pub author: Contributor,
pub committer: Contributor,
}

#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct DetailedGitRepo {
/// The local repo
local: LocalGitRepo,
/// Optional additional information for the query, hash in this case
pub details: String,
}

impl Display for Commit {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.hash)
}
}

#[query]
async fn commit_identity(engine: &mut PluginEngine, key: DetailedGitRepo) -> Result<bool> {
let value = engine
.query("mitre/git/contributors_for_commit", key)
.await
.map_err(|e| {
log::error!("failed to get last commits for identity metric: {}", e);
Error::UnspecifiedQueryState
})?;
let ccv = serde_json::from_value::<CommitContributorView>(value)
.map_err(Error::InvalidJsonInQueryOutput)?;
Ok(ccv.author == ccv.committer)
}

#[query(default)]
async fn identity(engine: &mut PluginEngine, key: Target) -> Result<Vec<bool>> {
// Get the commits for the source.
let repo = key.local;
let value = engine
.query("mitre/git/commits", repo.clone())
.await
.map_err(|e| {
log::error!("failed to get last commits for identity metric: {}", e);
Error::UnspecifiedQueryState
})?;
let commits: Vec<Commit> =
serde_json::from_value(value).map_err(|_| Error::UnexpectedPluginQueryInputFormat)?;
let mut res = vec![];
for c in commits {
let key = DetailedGitRepo {
local: repo.clone(),
details: c.hash,
};
res.push(commit_identity(engine, key).await?);
}
Ok(res)
}

#[derive(Clone, Debug, Default)]
struct IdentityPlugin {
policy_conf: OnceLock<Option<u64>>,
}

impl Plugin for IdentityPlugin {
const PUBLISHER: &'static str = "mitre";
const NAME: &'static str = "identity";

fn set_config(&self, config: Value) -> StdResult<(), ConfigError> {
// Deserialize the config struct
let conf =
serde_json::from_value::<Config>(config).map_err(|e| ConfigError::Unspecified {
message: e.to_string(),
})?;
self.policy_conf
.set(conf.percent_threshold)
.map_err(|_| ConfigError::Unspecified {
message: "plugin was already configured".to_string(),
})?;
Ok(())
}

fn default_policy_expr(&self) -> Result<String> {
match self.policy_conf.get() {
None => Err(Error::UnspecifiedQueryState),
// If no policy vars, we have no default expr
Some(None) => Ok("".to_owned()),
// Use policy config vars to construct a default expr
Some(Some(percent_threshold)) => Ok(format!(
"(lte (divz (count (filter (eq #t) $)) (count $)) {})",
percent_threshold
)),
}
}

fn explain_default_query(&self) -> Result<Option<String>> {
Ok(Some(
"Returns whether each commit in the repo was commited and authored by the same person"
.to_owned(),
))
}

queries! {}
}

#[derive(Parser, Debug)]
struct Args {
#[arg(long)]
port: u16,
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let args = Args::try_parse().unwrap();
PluginServer::register(IdentityPlugin::default())
.listen(args.port)
.await
}

#[cfg(test)]
mod test {
use super::*;

fn target() -> Target {
let local = LocalGitRepo {
git_ref: "HEAD".to_owned(),
path: ".".to_owned(),
};
Target {
specifier: "".to_owned(),
local,
remote: None,
package: None,
}
}

fn mock() -> Result<MockResponses> {
let target = target();
let local = target.local.clone();
let detailed1 = DetailedGitRepo {
local: local.clone(),
details: "abc123".to_owned(),
};
let detailed2 = DetailedGitRepo {
local: local.clone(),
details: "def456".to_owned(),
};
let committer = Contributor {
name: "John Doe".to_owned(),
email: "[email protected]".to_owned(),
};
let author = Contributor {
name: "Jane Doe".to_owned(),
email: "[email protected]".to_owned(),
};
let mut res = MockResponses::new();
let commit1 = Commit {
hash: "abc123".to_owned(),
written_on: Ok("10/23/2024".to_owned()),
committed_on: Ok("10/23/2024".to_owned()),
};
let commit2 = Commit {
hash: "def456".to_owned(),
written_on: Ok("10/23/2024".to_owned()),
committed_on: Ok("10/23/2024".to_owned()),
};
let commits = vec![commit1.clone(), commit2.clone()];
res.insert("mitre/git/commits", local, Ok(commits))?;
res.insert(
"mitre/git/contributors_for_commit",
detailed1,
Ok(CommitContributorView {
commit: commit1.clone(),
author: committer.clone(),
committer: committer.clone(),
}),
)?;
res.insert(
"mitre/git/contributors_for_commit",
detailed2,
Ok(CommitContributorView {
commit: commit2.clone(),
author: author.clone(),
committer: committer.clone(),
}),
)?;
Ok(res)
}

#[tokio::test]
async fn test_identity() {
let mut engine = PluginEngine::mock(mock().unwrap());

let res = identity(&mut engine, target()).await.unwrap();
assert_eq!(vec![true, false], res);
}
}

0 comments on commit 0d4312d

Please sign in to comment.