use crate::adapters::{PulldownMarkdownParser, RegexPatternMatcher, StdFileSystemProvider};
use crate::analyzer::SkillDocument;
use crate::artifact_graph::ArtifactGraph;
use crate::policy::{BaselineFile, DispositionOverlay, PolicyFile, WaiverFile};
use crate::ports::{FileSystemProvider, MarkdownParser};
use crate::rules::{default_external_rule_dirs, RuleEngine};
use crate::scanner_support::{
load_optional_baseline, load_optional_disposition, load_optional_policy, load_optional_waivers,
};
pub use crate::scanner_types::{
ArtifactMetadata, PackageScanResult, ScanError, ScanErrorEntry, ScanOptions, ScanResult,
ScanTargetMode,
};
use crate::services::{ArtifactOrchestratorService, FileDiscoveryService, ScanFilterService};
use crate::{scanner_execution, scanner_graph};
use std::path::Path;
use std::sync::Arc;
type EngineAndPolicy = (
RuleEngine<RegexPatternMatcher>,
Option<BaselineFile>,
Option<WaiverFile>,
Option<PolicyFile>,
Option<DispositionOverlay>,
);
fn build_engine_and_policy<F: FileSystemProvider>(
fs: &F,
options: &ScanOptions,
) -> Result<EngineAndPolicy, ScanError> {
let runtime_overlay_dirs = default_external_rule_dirs();
let mut engine = RuleEngine::with_defaults_and_matcher(
Arc::new(RegexPatternMatcher::new()),
fs,
&runtime_overlay_dirs,
)?;
engine.set_strict_mode(options.strict_rules);
if let Some(ref rules_dir) = options.rules_dir {
engine.load_from_dir(fs, rules_dir)?;
}
let baseline = load_optional_baseline(fs, options.baseline_path.as_deref())?;
let waivers = load_optional_waivers(fs, options.waivers_path.as_deref())?;
let policy = load_optional_policy(fs, options.policy_path.as_deref())?;
let disposition = load_optional_disposition(fs, options.disposition_path.as_deref())?;
Ok((engine, baseline, waivers, policy, disposition))
}
pub struct Scanner<
F: FileSystemProvider = StdFileSystemProvider,
P: MarkdownParser = PulldownMarkdownParser,
> {
engine: RuleEngine<RegexPatternMatcher>,
artifact_orchestration: ArtifactOrchestratorService,
file_discovery: FileDiscoveryService<F>,
filter_service: ScanFilterService,
parser: P,
}
pub type DefaultScanner = Scanner<StdFileSystemProvider, PulldownMarkdownParser>;
impl Scanner<StdFileSystemProvider, PulldownMarkdownParser> {
#[must_use = "Scanner::new() returns a Result that should be used"]
pub fn new() -> Result<Self, ScanError> {
Self::with_std_adapters(ScanOptions::default())
}
#[must_use = "Scanner::with_std_adapters() returns a Result that should be used"]
pub fn with_std_adapters(options: ScanOptions) -> Result<Self, ScanError> {
let fs = StdFileSystemProvider::new();
let (engine, baseline, waivers, policy, disposition) =
build_engine_and_policy(&fs, &options)?;
Ok(Self {
engine,
artifact_orchestration: ArtifactOrchestratorService::new(),
file_discovery: FileDiscoveryService::with_fs_provider(options.recursive, fs),
filter_service: ScanFilterService::with_policy_state(
options,
baseline,
waivers,
policy,
disposition,
),
parser: PulldownMarkdownParser::new(),
})
}
}
impl<F: FileSystemProvider, P: MarkdownParser> Scanner<F, P> {
#[must_use = "Scanner::with_custom_adapters() returns a Result that should be used"]
pub fn with_custom_adapters(
options: ScanOptions,
fs_provider: F,
parser: P,
) -> Result<Self, ScanError> {
let (engine, baseline, waivers, policy, disposition) =
build_engine_and_policy(&fs_provider, &options)?;
Ok(Self {
engine,
artifact_orchestration: ArtifactOrchestratorService::new(),
file_discovery: FileDiscoveryService::with_fs_provider(options.recursive, fs_provider),
filter_service: ScanFilterService::with_policy_state(
options,
baseline,
waivers,
policy,
disposition,
),
parser,
})
}
pub(crate) fn engine(&self) -> &RuleEngine<RegexPatternMatcher> {
&self.engine
}
pub(crate) fn artifact_orchestration(&self) -> &ArtifactOrchestratorService {
&self.artifact_orchestration
}
pub(crate) fn file_discovery(&self) -> &FileDiscoveryService<F> {
&self.file_discovery
}
pub(crate) fn filter_service(&self) -> &ScanFilterService {
&self.filter_service
}
pub(crate) fn parser(&self) -> &P {
&self.parser
}
pub(crate) fn build_artifact_graph(&self, doc: &SkillDocument) -> ArtifactGraph {
scanner_graph::build_artifact_graph::<F>(
&self.artifact_orchestration,
self.file_discovery.fs_provider(),
doc,
)
}
pub fn scan_file(&self, path: impl AsRef<Path>) -> Result<ScanResult, ScanError> {
let path = path.as_ref();
if !self.file_discovery.fs_provider().exists(path) {
return Err(ScanError::PathNotFound(path.to_path_buf()));
}
scanner_execution::scan_document_path(self, path)
}
pub fn scan_skill_file(&self, path: impl AsRef<Path>) -> Result<ScanResult, ScanError> {
let path = path.as_ref();
if !self.file_discovery.fs_provider().exists(path) {
return Err(ScanError::PathNotFound(path.to_path_buf()));
}
if !FileDiscoveryService::<F>::is_explicit_skill_file(path) {
return Err(ScanError::InvalidSkillEntrypoint(path.to_path_buf()));
}
scanner_execution::scan_document_path(self, path)
}
pub fn scan_package(&self, path: impl AsRef<Path>) -> Result<PackageScanResult, ScanError> {
let path = path.as_ref();
let fs = self.file_discovery.fs_provider();
if !fs.exists(path) {
return Err(ScanError::PathNotFound(path.to_path_buf()));
}
if fs.is_file(path) {
return Ok(match self.scan_file(path) {
Ok(result) => PackageScanResult {
results: vec![result],
errors: Vec::new(),
},
Err(err) => PackageScanResult {
results: Vec::new(),
errors: vec![crate::scanner_types::ScanErrorEntry {
path: path.to_path_buf(),
error: err.to_string(),
}],
},
});
}
let targets = scanner_execution::discover_package_targets(self, path)?;
let mut pkg_result = PackageScanResult::new();
for target in targets {
match self.scan_file(&target) {
Ok(result) => pkg_result.results.push(result),
Err(err) => {
pkg_result
.errors
.push(crate::scanner_types::ScanErrorEntry {
path: target.clone(),
error: err.to_string(),
});
tracing::warn!("Failed to scan {}: {}", target.display(), err);
}
}
}
Ok(pkg_result)
}
pub fn scan(&self, path: impl AsRef<Path>) -> Result<PackageScanResult, ScanError> {
let path = path.as_ref();
match self.filter_service.target_mode() {
ScanTargetMode::Auto => {
let fs = self.file_discovery.fs_provider();
if fs.is_file(path) {
let result = self.scan_file(path)?;
Ok(PackageScanResult {
results: vec![result],
errors: Vec::new(),
})
} else if fs.is_dir(path) {
self.scan_package(path)
} else {
Err(ScanError::PathNotFound(path.to_path_buf()))
}
}
ScanTargetMode::File => {
let result = self.scan_file(path)?;
Ok(PackageScanResult {
results: vec![result],
errors: Vec::new(),
})
}
ScanTargetMode::Package => self.scan_package(path),
}
}
pub fn rule_count(&self) -> usize {
self.engine.rule_count()
}
pub fn rules(&self) -> Vec<&crate::rules::Rule> {
self.engine.rules()
}
}
#[cfg(test)]
mod basic_tests;
#[cfg(test)]
mod capabilities_tests;
#[cfg(test)]
mod manifest_tests;