pub(crate) mod dispatch;
mod instructions;
pub(crate) mod manifests;
pub(crate) mod network;
pub(crate) mod scripts;
use crate::detectors::patterns;
use crate::artifact_graph::{
ArtifactCapability, ArtifactCapabilityFact, ArtifactCapabilitySource, ArtifactRelation,
};
use crate::findings::{
ArtifactKind, EvidenceKind, Finding, MatchTarget, RecommendedAction, Severity, ThreatCategory,
};
use std::path::{Path, PathBuf};
pub struct ArtifactOrchestratorService;
#[derive(Debug, Clone)]
pub struct ArtifactLink {
pub target: String,
pub relation: ArtifactRelation,
}
impl ArtifactOrchestratorService {
#[must_use]
pub fn new() -> Self {
Self
}
pub fn analyze(
&self,
path: &Path,
content: &str,
sibling_files: &[PathBuf],
document: Option<&crate::analyzer::SkillDocument>,
) -> Vec<Finding> {
dispatch::analyze(self, path, content, sibling_files, document)
}
pub(crate) fn is_opaque_mcp_endpoint(&self, content: &str) -> bool {
patterns::RE_OPAQUE_MCP_ENDPOINT.is_match(content)
}
pub(crate) fn mcp_declares_no_auth(&self, content: &str) -> bool {
patterns::RE_MCP_NO_AUTH.is_match(content)
}
pub(crate) fn mcp_declares_inline_secret(&self, content: &str) -> bool {
patterns::RE_MCP_INLINE_SECRET.is_match(content)
}
pub(crate) fn mcp_declares_permissive_tools(&self, content: &str) -> bool {
patterns::RE_MCP_PERMISSIVE_TOOLS.is_match(content)
}
pub(crate) fn extract_mcp_tool_names(&self, content: &str) -> Vec<String> {
let mut tools = Vec::new();
if let Some(array_match) = patterns::RE_MCP_TOOLS_ARRAY
.captures_iter(content)
.into_iter()
.next()
.and_then(|captures| captures.get(1).cloned())
{
for capture in patterns::RE_QUOTED_TOOL_NAME.captures_iter(&array_match.matched_text) {
if let Some(name) = capture.get(1) {
let value = name.matched_text.clone();
if !tools.contains(&value) {
tools.push(value);
}
}
}
}
tools
}
pub fn infer_relations(&self, path: &Path, content: &str) -> Vec<ArtifactLink> {
dispatch::infer_relations(self, path, content)
}
pub fn infer_capabilities(&self, path: &Path, content: &str) -> Vec<ArtifactCapabilityFact> {
dispatch::infer_capabilities(self, path, content)
}
pub fn expected_lockfiles(&self, path: &Path, content: &str) -> Vec<&'static str> {
dispatch::expected_lockfiles(self, path, content)
}
pub(crate) fn permission_and_network_findings(
&self,
path: &Path,
content: &str,
artifact_kind: ArtifactKind,
) -> Vec<Finding> {
instructions::permission_and_network_findings(self, path, content, artifact_kind, None)
}
pub(crate) fn declared_capability(capability: ArtifactCapability) -> ArtifactCapabilityFact {
ArtifactCapabilityFact {
capability,
source: ArtifactCapabilitySource::Declared,
}
}
pub(crate) fn observed_capability(capability: ArtifactCapability) -> ArtifactCapabilityFact {
ArtifactCapabilityFact {
capability,
source: ArtifactCapabilitySource::Observed,
}
}
pub(crate) fn missing_lockfile_findings(
&self,
path: &Path,
sibling_files: &[PathBuf],
expected_lockfiles: &[&str],
rule_id: &str,
reason: &str,
) -> Vec<Finding> {
let artifact_path = path.display().to_string();
let has_lockfile = sibling_files.iter().any(|candidate| {
candidate
.file_name()
.and_then(|name| name.to_str())
.map(|name| {
expected_lockfiles
.iter()
.any(|expected| name.eq_ignore_ascii_case(expected))
})
.unwrap_or(false)
});
if has_lockfile {
return Vec::new();
}
vec![Finding::builder(rule_id, ThreatCategory::SupplyChain)
.severity(Severity::Low)
.action(RecommendedAction::Log)
.evidence_kind(EvidenceKind::Context)
.artifact(ArtifactKind::PackageManifest, Some(artifact_path.clone()))
.matched_on(MatchTarget::ReferencedFile {
path: artifact_path,
})
.match_value(expected_lockfiles.join(", "))
.reason(reason)
.build()]
}
pub(crate) fn generic_url_relations(&self, content: &str) -> Vec<ArtifactLink> {
let mut links = Vec::new();
for matched in patterns::RE_GENERIC_URL.find_matches(content) {
links.push(ArtifactLink {
target: matched.matched_text,
relation: ArtifactRelation::ConnectsTo,
});
}
links
}
}
impl Default for ArtifactOrchestratorService {
fn default() -> Self {
Self::new()
}
}