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::SchemaObject 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, Error, Result};
83 pub use crate::server::{Host, PluginServer, QueryResult};
84 pub use crate::{DynQuery, NamedQuery, Plugin, Query, QuerySchema, QueryTarget};
85 // Re-export macros
86 #[cfg(feature = "macros")]
87 #[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
88 pub use crate::macros::{queries, query};
89
90 #[cfg(feature = "mock_engine")]
91 #[cfg_attr(docsrs, doc(cfg(feature = "mock_engine")))]
92 pub use crate::engine::MockResponses;
93}
94
95/// Re-export of user-facing third-party dependencies
96pub mod deps {
97 pub use jiff::{Span, Zoned};
98 pub use schemars::{schema::SchemaObject as JsonSchema, schema_for};
99 pub use serde_json::{from_str, from_value, to_value, Value};
100 pub use tonic::async_trait;
101}
102
103/// Identifies the target plugin and endpoint of a Hipcheck query.
104///
105/// The `publisher` and `plugin` fields are necessary from Hipcheck core's perspective to identify
106/// a plugin process. Plugins may define one or more query endpoints, and may include an unnamed
107/// endpoint as the "default", hence why the `query` field is optional. `QueryTarget` implements
108/// `FromStr` so it can be parsed from strings of the format `"publisher/plugin[/query]"`, where
109/// the bracketed substring is optional.
110#[derive(Debug, Clone, PartialEq, Eq, Hash)]
111pub struct QueryTarget {
112 pub publisher: String,
113 pub plugin: String,
114 pub query: Option<String>,
115}
116
117impl FromStr for QueryTarget {
118 type Err = Error;
119
120 fn from_str(s: &str) -> StdResult<Self, Self::Err> {
121 let parts: Vec<&str> = s.split('/').collect();
122 match parts.as_slice() {
123 [publisher, plugin, query] => Ok(Self {
124 publisher: publisher.to_string(),
125 plugin: plugin.to_string(),
126 query: Some(query.to_string()),
127 }),
128 [publisher, plugin] => Ok(Self {
129 publisher: publisher.to_string(),
130 plugin: plugin.to_string(),
131 query: None,
132 }),
133 _ => Err(Error::InvalidQueryTargetFormat),
134 }
135 }
136}
137impl std::fmt::Display for QueryTarget {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 match &self.query {
140 Some(query) => write!(f, "{}/{}/{}", self.publisher, self.plugin, query),
141 None => write!(f, "{}/{}", self.publisher, self.plugin),
142 }
143 }
144}
145
146impl TryInto<QueryTarget> for &str {
147 type Error = Error;
148 fn try_into(self) -> StdResult<QueryTarget, Self::Error> {
149 QueryTarget::from_str(self)
150 }
151}
152
153/// Describes the signature of a particular `NamedQuery`.
154///
155/// Instances of this type are usually created by the default implementation of `Plugin::schemas()`
156/// and would not need to be created by hand unless you are doing something very unorthodox.
157pub struct QuerySchema {
158 /// The name of the query being described.
159 query_name: &'static str,
160
161 /// The query's input schema as a `schemars::schema::SchemaObject`.
162 input_schema: JsonSchema,
163
164 /// The query's output schema as a `schemars::schema::SchemaObject`.
165 output_schema: JsonSchema,
166}
167
168/// A `Query` trait object.
169pub type DynQuery = Box<dyn Query>;
170
171/// Pairs a query endpoint name with a particular `Query` trait implementation.
172///
173/// Since the `Query` trait needs to be made into a trait object, we can't use a static associated
174/// string to store the query's name in the trait itself. This object wraps a `Query` trait object
175/// and allows us to associate a name with it so that when the plugin receives a query from
176/// Hipcheck core, it can look up the proper behavior to invoke.
177pub struct NamedQuery {
178 /// The name of the query.
179 pub name: &'static str,
180
181 /// The `Query` trait object.
182 pub inner: DynQuery,
183}
184
185impl NamedQuery {
186 /// Returns whether the current query is the plugin's default query, determined by whether the
187 /// query name is empty.
188 fn is_default(&self) -> bool {
189 self.name.is_empty()
190 }
191}
192
193/// Defines a single query endpoint for the plugin.
194#[tonic::async_trait]
195pub trait Query: Send {
196 /// Get the input schema for the query as a `schemars::schema::SchemaObject`.
197 fn input_schema(&self) -> JsonSchema;
198
199 /// Get the output schema for the query as a `schemars::schema::SchemaObject`.
200 fn output_schema(&self) -> JsonSchema;
201
202 /// Run the query endpoint logic on `input`, returning a JSONified return value on success.
203 /// The `PluginEngine` reference allows the endpoint to query other Hipcheck plugins by
204 /// calling `engine::query()`.
205 async fn run(&self, engine: &mut PluginEngine, input: JsonValue) -> Result<JsonValue>;
206}
207
208/// The core trait that a plugin author must implement using the Hipcheck SDK.
209///
210/// Declares basic information about the plugin and its query endpoints, and accepts a
211/// configuration map from Hipcheck core.
212pub trait Plugin: Send + Sync + 'static {
213 /// The name of the plugin publisher.
214 const PUBLISHER: &'static str;
215
216 /// The name of the plugin.
217 const NAME: &'static str;
218
219 /// Handle setting configuration. The `config` parameter is a JSON object of `String, String`
220 /// pairs.
221 fn set_config(&self, config: JsonValue) -> StdResult<(), ConfigError>;
222
223 /// Get the plugin's default policy expression. This will only ever be called after
224 /// `Plugin::set_config()`. For more information on policy expression syntax, see the Hipcheck
225 /// website.
226 fn default_policy_expr(&self) -> Result<String>;
227
228 /// Get an unstructured description of what is returned by the plugin's default query.
229 fn explain_default_query(&self) -> Result<Option<String>>;
230
231 /// Get all the queries supported by the plugin. Each query endpoint in a plugin will have its
232 /// own `trait Query` implementation. This function should return an iterator containing one
233 /// `NamedQuery` instance for each `trait Query` implementation defined by the plugin author.
234 fn queries(&self) -> impl Iterator<Item = NamedQuery>;
235
236 /// Get the plugin's default query, if it has one. The default query is a `NamedQuery` with an
237 /// empty `name` string. In most cases users should not need to override the default
238 /// implementation.
239 fn default_query(&self) -> Option<DynQuery> {
240 self.queries()
241 .find_map(|named| named.is_default().then_some(named.inner))
242 }
243
244 /// Get all schemas for queries provided by the plugin. In most cases users should not need to
245 /// override the default implementation.
246 fn schemas(&self) -> impl Iterator<Item = QuerySchema> {
247 self.queries().map(|query| QuerySchema {
248 query_name: query.name,
249 input_schema: query.inner.input_schema(),
250 output_schema: query.inner.output_schema(),
251 })
252 }
253}
254
255/// Initializes a `tracing-subscriber` for plugin logging and forwards logs to Hipcheck core.
256///
257/// Initializes a `tracing-subscriber` which writes log messages produced via the `tracing` crate macros to stdout/stderr.
258/// These plugin logs are then piped to Hipcheck core's stdout/stderr log output during execution.
259///
260/// `tracing-subscriber` uses an `EnvFilter` to filter logs to up to the log-level passed as argument `log-level`
261/// to the plugin by `HC Core`.
262///
263/// log_forwarding() is enabled as a default feature, but could be disabled and replaced by setting `default-features = false`
264/// in the `dependencies` section of a plugin's `Cargo.toml` prior to compilation.
265#[cfg(feature = "log_forwarding")]
266pub fn init_tracing_logger(log_level: LogLevel) {
267 tracing_subscriber::fmt()
268 .json()
269 .with_writer(std::io::stderr)
270 .with_env_filter(log_level.to_string())
271 .init();
272}