Skip to main content

hipcheck_sdk/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2
3#![allow(unexpected_cfgs)]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5#![cfg_attr(docsrs, allow(unused_attributes))]
6
7//! Hipcheck Plugin SDK in Rust.
8//!
9//! ## What is Hipcheck?
10//! [Hipcheck][hipcheck] is a command line interface (CLI) tool for analyzing open source software
11//! packages and source repositories to understand their software supply chain risk. It analyzes a
12//! project's software development practices and detects active supply chain attacks to give you
13//! both a long-term and immediate picture of the risk from using a package.
14//!
15//! Part of Hipcheck's value is its [plugin system][hipcheck_plugins], which allows anyone to write
16//! a new data source or analysis component, or build even higher level analyses off of the results
17//! of multiple other components.
18//!
19//! ## The Plugin SDK
20//! This crate is a Rust SDK to help developers focus on writing the essential logic of their
21//! Hipcheck plugins instead of worrying about session management or communication with Hipcheck
22//! core. The essential steps of using this SDK are to implement the `Query` trait for each query
23//! endpoint you wish to support, then implement the `Plugin` trait to tie your plugin together and
24//! describe things like configuration parameters.
25//!
26//! For more, see our [detailed guide][web_sdk_docs] on writing plugins using this crate.
27//!
28//! [hipcheck]: https://hipcheck.mitre.org/
29//! [hipcheck_plugins]: https://hipcheck.mitre.org/docs/guide/making-plugins/creating-a-plugin/
30//! [web_sdk_docs]: https://hipcheck.mitre.org/docs/guide/making-plugins/rust-sdk/
31
32use crate::error::{ConfigError, Error, Result};
33pub use engine::PluginEngine;
34pub use engine::QueryBuilder;
35pub use hipcheck_common::types::LogLevel;
36use schemars::Schema as JsonSchema;
37use serde_json::Value as JsonValue;
38pub use server::PluginServer;
39use std::result::Result as StdResult;
40use std::str::FromStr;
41
42#[cfg(feature = "macros")]
43#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
44/// Macros for simplifying `Query` and `Plugin` trait implementations
45pub mod macros {
46	pub use hipcheck_sdk_macros::*;
47}
48
49/// The trait used to deserialized plugin config input from the Policy File.
50/// The trait is applied to a plugin RawConfig struct and works in tandem with
51/// the derive_plugin_config procedural macro re-imported to this sdk crate
52/// via hipcheck_sdk_macros.
53pub trait PluginConfig<'de> {
54	fn deserialize(config: &serde_json::Value) -> StdResult<Self, ConfigError>
55	where
56		Self: Sized;
57}
58
59#[cfg(feature = "print-timings")]
60mod benchmarking;
61
62mod engine;
63pub mod error;
64mod server;
65
66#[cfg(feature = "mock_engine")]
67#[cfg_attr(docsrs, doc(cfg(feature = "mock_engine")))]
68/// Tools for unit-testing plugin `Query` implementations
69pub mod mock {
70	pub use crate::engine::MockResponses;
71}
72
73/// The definitions of Hipcheck's analysis `Target` object and its sub-types for use in writing
74/// query endpoints.
75pub mod types;
76
77/// A utility module containing everything needed to write a plugin, just write `use
78/// hipcheck_sdk::prelude::*`.
79pub mod prelude {
80	pub use crate::deps::*;
81	pub use crate::engine::{PluginEngine, QueryBuilder};
82	pub use crate::error::{ConfigError, ConfigResult, Error, Result};
83	pub use crate::server::{Host, PluginServer, QueryResult};
84	pub use crate::types::{KnownRemote, RemoteGitRepo};
85	pub use crate::{DynQuery, NamedQuery, Plugin, Query, QuerySchema, QueryTarget};
86	// Re-export macros
87	#[cfg(feature = "macros")]
88	#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
89	pub use crate::macros::{queries, query};
90
91	#[cfg(feature = "mock_engine")]
92	#[cfg_attr(docsrs, doc(cfg(feature = "mock_engine")))]
93	pub use crate::engine::MockResponses;
94}
95
96/// Re-export of user-facing third-party dependencies
97pub mod deps {
98	pub use jiff::{Span, Zoned};
99	pub use schemars::{Schema as JsonSchema, schema_for};
100	pub use serde_json::{Value, from_str, from_value, to_value};
101	pub use tonic::async_trait;
102}
103
104/// Identifies the target plugin and endpoint of a Hipcheck query.
105///
106/// The `publisher` and `plugin` fields are necessary from Hipcheck core's perspective to identify
107/// a plugin process. Plugins may define one or more query endpoints, and may include an unnamed
108/// endpoint as the "default", hence why the `query` field is optional. `QueryTarget` implements
109/// `FromStr` so it can be parsed from strings of the format `"publisher/plugin[/query]"`, where
110/// the bracketed substring is optional.
111#[derive(Debug, Clone, PartialEq, Eq, Hash)]
112pub struct QueryTarget {
113	pub publisher: String,
114	pub plugin: String,
115	pub query: Option<String>,
116}
117
118impl FromStr for QueryTarget {
119	type Err = Error;
120
121	fn from_str(s: &str) -> StdResult<Self, Self::Err> {
122		let parts: Vec<&str> = s.split('/').collect();
123		match parts.as_slice() {
124			[publisher, plugin, query] => Ok(Self {
125				publisher: publisher.to_string(),
126				plugin: plugin.to_string(),
127				query: Some(query.to_string()),
128			}),
129			[publisher, plugin] => Ok(Self {
130				publisher: publisher.to_string(),
131				plugin: plugin.to_string(),
132				query: None,
133			}),
134			_ => Err(Error::InvalidQueryTargetFormat),
135		}
136	}
137}
138impl std::fmt::Display for QueryTarget {
139	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140		match &self.query {
141			Some(query) => write!(f, "{}/{}/{}", self.publisher, self.plugin, query),
142			None => write!(f, "{}/{}", self.publisher, self.plugin),
143		}
144	}
145}
146
147impl TryInto<QueryTarget> for &str {
148	type Error = Error;
149	fn try_into(self) -> StdResult<QueryTarget, Self::Error> {
150		QueryTarget::from_str(self)
151	}
152}
153
154/// Describes the signature of a particular `NamedQuery`.
155///
156/// Instances of this type are usually created by the default implementation of `Plugin::schemas()`
157/// and would not need to be created by hand unless you are doing something very unorthodox.
158pub struct QuerySchema {
159	/// The name of the query being described.
160	query_name: &'static str,
161
162	/// The query's input schema as a `schemars::schema::SchemaObject`.
163	input_schema: JsonSchema,
164
165	/// The query's output schema as a `schemars::schema::SchemaObject`.
166	output_schema: JsonSchema,
167}
168
169/// A `Query` trait object.
170pub type DynQuery = Box<dyn Query>;
171
172/// Pairs a query endpoint name with a particular `Query` trait implementation.
173///
174/// Since the `Query` trait needs to be made into a trait object, we can't use a static associated
175/// string to store the query's name in the trait itself. This object wraps a `Query` trait object
176/// and allows us to associate a name with it so that when the plugin receives a query from
177/// Hipcheck core, it can look up the proper behavior to invoke.
178pub struct NamedQuery {
179	/// The name of the query.
180	pub name: &'static str,
181
182	/// The `Query` trait object.
183	pub inner: DynQuery,
184}
185
186impl NamedQuery {
187	/// Returns whether the current query is the plugin's default query, determined by whether the
188	/// query name is empty.
189	fn is_default(&self) -> bool {
190		self.name.is_empty()
191	}
192}
193
194/// Defines a single query endpoint for the plugin.
195#[tonic::async_trait]
196pub trait Query: Send {
197	/// Get the input schema for the query as a `schemars::schema::SchemaObject`.
198	fn input_schema(&self) -> JsonSchema;
199
200	/// Get the output schema for the query as a `schemars::schema::SchemaObject`.
201	fn output_schema(&self) -> JsonSchema;
202
203	/// Run the query endpoint logic on `input`, returning a JSONified return value on success.
204	/// The `PluginEngine` reference allows the endpoint to query other Hipcheck plugins by
205	/// calling `engine::query()`.
206	async fn run(&self, engine: &mut PluginEngine, input: JsonValue) -> Result<JsonValue>;
207}
208
209/// The core trait that a plugin author must implement using the Hipcheck SDK.
210///
211/// Declares basic information about the plugin and its query endpoints, and accepts a
212/// configuration map from Hipcheck core.
213pub trait Plugin: Send + Sync + 'static {
214	/// The name of the plugin publisher.
215	const PUBLISHER: &'static str;
216
217	/// The name of the plugin.
218	const NAME: &'static str;
219
220	/// Handle setting configuration. The `config` parameter is a JSON object of `String, String`
221	/// pairs.
222	fn set_config(&self, config: JsonValue) -> StdResult<(), ConfigError>;
223
224	/// Get the plugin's default policy expression. This will only ever be called after
225	/// `Plugin::set_config()`. For more information on policy expression syntax, see the Hipcheck
226	/// website.
227	fn default_policy_expr(&self) -> Result<String> {
228		Ok(String::new())
229	}
230
231	/// Get an unstructured description of what is returned by the plugin's default query.
232	fn explain_default_query(&self) -> Result<Option<String>> {
233		Ok(None)
234	}
235
236	/// Get all the queries supported by the plugin. Each query endpoint in a plugin will have its
237	/// own `trait Query` implementation. This function should return an iterator containing one
238	/// `NamedQuery` instance for each `trait Query` implementation defined by the plugin author.
239	fn queries(&self) -> impl Iterator<Item = NamedQuery>;
240
241	/// Get the plugin's default query, if it has one. The default query is a `NamedQuery` with an
242	/// empty `name` string. In most cases users should not need to override the default
243	/// implementation.
244	fn default_query(&self) -> Option<DynQuery> {
245		self.queries()
246			.find_map(|named| named.is_default().then_some(named.inner))
247	}
248
249	/// Get all schemas for queries provided by the plugin. In most cases users should not need to
250	/// override the default implementation.
251	fn schemas(&self) -> impl Iterator<Item = QuerySchema> {
252		self.queries().map(|query| QuerySchema {
253			query_name: query.name,
254			input_schema: query.inner.input_schema(),
255			output_schema: query.inner.output_schema(),
256		})
257	}
258}
259
260/// Initializes a `tracing-subscriber` for plugin logging and forwards logs to Hipcheck core.
261///
262/// Initializes a `tracing-subscriber` which writes log messages produced via the `tracing` crate macros to stdout/stderr.
263/// These plugin logs are then piped to Hipcheck core's stdout/stderr log output during execution.
264///
265/// `tracing-subscriber` uses an `EnvFilter` to filter logs to up to the log-level passed as argument `log-level`
266/// to the plugin by `HC Core`.
267///
268/// log_forwarding() is enabled as a default feature, but could be disabled and replaced by setting `default-features = false`
269/// in the `dependencies` section of a plugin's `Cargo.toml` prior to compilation.
270#[cfg(feature = "log_forwarding")]
271pub fn init_tracing_logger(log_level: LogLevel) {
272	tracing_subscriber::fmt()
273		.json()
274		.with_writer(std::io::stderr)
275		.with_env_filter(log_level.to_string())
276		.init();
277}