pub mod engine;
pub mod extract;
pub mod input;
pub mod policy;
pub mod render;
pub mod report;
pub mod rules;
pub mod skill;
pub mod views;
pub mod waiver;
use std::ops::Range;
pub use report::{Coverage, Finding, Report};
pub use waiver::{Approval, VerifyOutcome, Waiver};
pub const SCHEMA_VERSION: &str = "1.0.0";
pub const TOOL_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Profile {
PublicBugReport,
CommitMessage,
Changelog,
ReleaseNotes,
Readme,
ApiDocs,
CargoMetadata,
InternalDoc,
}
impl Profile {
pub const ALL: [Profile; 8] = [
Profile::PublicBugReport,
Profile::CommitMessage,
Profile::Changelog,
Profile::ReleaseNotes,
Profile::Readme,
Profile::ApiDocs,
Profile::CargoMetadata,
Profile::InternalDoc,
];
pub fn as_str(self) -> &'static str {
match self {
Profile::PublicBugReport => "public-bug-report",
Profile::CommitMessage => "commit-message",
Profile::Changelog => "changelog",
Profile::ReleaseNotes => "release-notes",
Profile::Readme => "readme",
Profile::ApiDocs => "api-docs",
Profile::CargoMetadata => "cargo-metadata",
Profile::InternalDoc => "internal-doc",
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Option<Profile> {
Profile::ALL.iter().copied().find(|p| p.as_str() == s)
}
pub fn index(self) -> usize {
self as usize
}
pub fn default_format(self) -> InputFormat {
match self {
Profile::CommitMessage => InputFormat::Commit,
Profile::CargoMetadata => InputFormat::Manifest,
_ => InputFormat::Markdown,
}
}
pub fn supported_formats(self) -> &'static [InputFormat] {
match self {
Profile::CommitMessage => &[InputFormat::Commit],
Profile::CargoMetadata => &[InputFormat::Manifest],
_ => &[InputFormat::Markdown, InputFormat::Text],
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum InputFormat {
Markdown,
Text,
Commit,
Manifest,
}
impl InputFormat {
pub fn as_str(self) -> &'static str {
match self {
InputFormat::Markdown => "markdown",
InputFormat::Text => "text",
InputFormat::Commit => "commit",
InputFormat::Manifest => "manifest",
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Option<InputFormat> {
match s {
"markdown" => Some(InputFormat::Markdown),
"text" => Some(InputFormat::Text),
"commit" => Some(InputFormat::Commit),
"manifest" => Some(InputFormat::Manifest),
_ => None,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum WaiverAuthority {
Human,
OrchestratorAgent,
}
#[derive(Clone, Debug)]
pub struct Limits {
pub max_bytes: usize,
pub max_findings: usize,
}
impl Default for Limits {
fn default() -> Self {
Limits {
max_bytes: 2 * 1024 * 1024,
max_findings: 10_000,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Deployment {
pub waiver_authority: Option<WaiverAuthority>,
pub demote: Vec<String>,
pub expected_version: Option<String>,
pub expected_license_wording: Option<String>,
pub expected_release_body: Option<String>,
pub scrub_overrides: Option<Vec<String>>,
pub exempt_comment_markers: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct Config {
pub profile: Profile,
pub input_format: InputFormat,
pub limits: Limits,
pub suggest: bool,
pub waivers: Vec<Waiver>,
pub deployment: Deployment,
pub now_unix: Option<i64>,
}
impl Config {
pub fn new(profile: Profile) -> Config {
Config {
profile,
input_format: profile.default_format(),
limits: Limits::default(),
suggest: false,
waivers: Vec::new(),
deployment: Deployment::default(),
now_unix: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AnalysisError {
Instrumentation(String),
UnsupportedInput(String),
Usage(String),
}
impl std::fmt::Display for AnalysisError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AnalysisError::Instrumentation(m) => write!(f, "instrumentation_error: {m}"),
AnalysisError::UnsupportedInput(m) => write!(f, "unsupported_input: {m}"),
AnalysisError::Usage(m) => write!(f, "usage error: {m}"),
}
}
}
impl std::error::Error for AnalysisError {}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Field {
Whole,
Subject,
Body,
Trailers,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Stance {
Apply,
Relax,
Off,
}
pub fn analyze(input: &[u8], config: &Config) -> Result<Report, AnalysisError> {
validate_config(config)?;
let compiled = engine::compiled()
.map_err(|e| AnalysisError::Instrumentation(format!("policy load failed: {e}")))?;
let prepared = input::prepare(input, config)?;
let doc = extract::build_doc(&prepared, config)?;
let norm = views::build_norm(&prepared.text, &doc);
let mut hits = engine::scan_all(compiled, &prepared, &doc, &norm, config)?;
rules::evaluate_structural(compiled, &prepared, &doc, &norm, config, &mut hits);
report::assemble(compiled, &prepared, &doc, &norm, config, hits)
}
fn validate_config(config: &Config) -> Result<(), AnalysisError> {
let compiled = engine::compiled()
.map_err(|e| AnalysisError::Instrumentation(format!("policy load failed: {e}")))?;
for id in &config.deployment.demote {
let rule = compiled
.pkg
.rules
.iter()
.find(|r| &r.id == id)
.ok_or_else(|| AnalysisError::Usage(format!("demote names unknown rule {id}")))?;
if rule.id == "SLOP-J001" {
return Err(AnalysisError::Usage(
"SLOP-J001 is never demotable".to_string(),
));
}
if rule.tier != policy::Tier::Candidate {
return Err(AnalysisError::Usage(format!(
"demotion of non-candidate rule {id} is not allowed"
)));
}
}
for w in &config.waivers {
if w.span.is_none() || w.expires.is_none() {
return Err(AnalysisError::Usage(format!(
"waiver for {} must be span-bound and carry an explicit expires; \
a span-less or non-expiring waiver is invalid",
w.rule_id
)));
}
}
if !config
.profile
.supported_formats()
.contains(&config.input_format)
{
return Err(AnalysisError::Usage(format!(
"format {} is not supported by profile {}",
config.input_format.as_str(),
config.profile.as_str()
)));
}
Ok(())
}
pub fn verify(input: &[u8], approval: &Approval, now_unix: i64) -> VerifyOutcome {
waiver::verify(input, approval, now_unix)
}
pub fn policy_digest() -> String {
policy::compute_digest()
}
pub(crate) fn widen_to_char_boundaries(s: &str, mut r: Range<usize>) -> Range<usize> {
if r.start > s.len() {
r.start = s.len();
}
if r.end > s.len() {
r.end = s.len();
}
while r.start > 0 && !s.is_char_boundary(r.start) {
r.start -= 1;
}
while r.end < s.len() && !s.is_char_boundary(r.end) {
r.end += 1;
}
if r.end < r.start {
r.end = r.start;
}
r
}