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 {
GeneralWriting,
BlogPost,
Email,
Report,
Doc,
SocialPost,
Comment,
}
impl Profile {
pub const ALL: [Profile; 7] = [
Profile::GeneralWriting,
Profile::BlogPost,
Profile::Email,
Profile::Report,
Profile::Doc,
Profile::SocialPost,
Profile::Comment,
];
pub fn as_str(self) -> &'static str {
match self {
Profile::GeneralWriting => "general-writing",
Profile::BlogPost => "blog-post",
Profile::Email => "email",
Profile::Report => "report",
Profile::Doc => "doc",
Profile::SocialPost => "social-post",
Profile::Comment => "comment",
}
}
#[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 {
InputFormat::Markdown
}
pub fn supported_formats(self) -> &'static [InputFormat] {
&[InputFormat::Markdown, InputFormat::Text]
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum InputFormat {
Markdown,
Text,
}
impl InputFormat {
pub fn as_str(self) -> &'static str {
match self {
InputFormat::Markdown => "markdown",
InputFormat::Text => "text",
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Option<InputFormat> {
match s {
"markdown" => Some(InputFormat::Markdown),
"text" => Some(InputFormat::Text),
_ => 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_license_wording: Option<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 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 code_blocks: Vec<Range<usize>> = doc
.regions
.iter()
.filter(|r| r.kind == extract::RegionKind::CodeBlock)
.map(|r| r.range.clone())
.collect();
if let Some((code_lines, nonblank)) = input::source_shape(&prepared.text, &code_blocks) {
return Err(AnalysisError::UnsupportedInput(format!(
"Input looks like a Rust source file: {code_lines} of {nonblank} lines \
outside code blocks carry code structure. Pass the prose, or wrap \
the code in a fenced block."
)));
}
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 first_char_span(s: &str) -> Range<usize> {
0..s.chars().next().map(char::len_utf8).unwrap_or(0)
}
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
}