feat(onboarding): artifact ingest + classifier + suite-integration seams
Steps 3-4 of the onboarding plan, plus the sibling-product reconciliation seams. Ingest (compliance-agent/src/ingest, #120): - ingest_all / ingest_artifact normalize each artifact to a working path + metadata. Every blob is SHA-256 hashed (content-addressed store, dedup) — that digest is also the tramiton reconciliation key. - git via GitOps reuse; zip archives + mobile packages extracted; firmware stored as blob; live URL / plaintext / container = metadata only. - IngestContext decoupled from the full AgentConfig (testable in isolation). Classify (compliance-agent/src/classify, #121): - FirmwareDetector port + TramitonCli (shell out `tramiton detect --json`, parse a mirrored BuildPlan subset — no dependency on the proprietary crate) + a deterministic MockFirmwareDetector so CI never needs the binary. - HeuristicClassifier: artifact-kind priors + source-marker fingerprinting (web/backend/mobile/desktop/PLC). - classify_target merges + ranks verdicts into a Classification. Suite-integration seams (compliance-core, #135/#136/#137): - Model: ExternalRef/ExternalSystem (reconcile with tramiton/werkpilot/breakpilot), ComplianceProfile/ComplianceFramework + default_compliance_profile per type. - Ports: EvidenceProvider (fetch external SBOM/VEX/lock/attestation) and ControlsProvider (built-in OSCAL vs breakpilot RAG). - TargetType now derives Hash; AgentConfig gains artifact_store_base_path. 44 unit tests (23 core + 8 ingest + 13 classify). Passes fmt + clippy -D warnings across agent, dashboard (server + web), and mcp. Additive; legacy paths untouched. Refs #118, #120, #121, #135, #136, #137. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
675c4ef699
commit
c6e82bc331
@@ -22,7 +22,7 @@ use super::scan::ScanType;
|
||||
/// Targets look endlessly varied but fall into a small enumerable set classified
|
||||
/// by where the analyzable signal lives. This drives the scan-applicability
|
||||
/// matrix and the onboarding wizard's type selection.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TargetType {
|
||||
/// Browser-facing web application (front end + server).
|
||||
@@ -411,6 +411,134 @@ pub struct TargetScanConfig {
|
||||
pub issue_tracker: Option<IssueTrackerConfig>,
|
||||
}
|
||||
|
||||
/// A sibling product in the company suite that may already hold authoritative
|
||||
/// data for a target. compliance-scanner reconciles with these rather than
|
||||
/// recomputing what they already know.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExternalSystem {
|
||||
/// Reproducible-build & firmware compliance engine (build plan, SBOM, VEX,
|
||||
/// attestation).
|
||||
Tramiton,
|
||||
/// Code assistant (downstream remediation consumer).
|
||||
Werkpilot,
|
||||
/// Compliance-controls RAG (atomic controls derived from laws).
|
||||
BreakpilotCompliance,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExternalSystem {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Tramiton => write!(f, "tramiton"),
|
||||
Self::Werkpilot => write!(f, "werkpilot"),
|
||||
Self::BreakpilotCompliance => write!(f, "breakpilot_compliance"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A link from this target to a record in a sibling product, used to reconcile
|
||||
/// existing evidence instead of recomputing it.
|
||||
///
|
||||
/// For tramiton, `project_id` is the shared cross-product key and
|
||||
/// `subject_sha256` matches a firmware artifact's [`Artifact::content_hash`]
|
||||
/// (which equals tramiton's `Artifact.sha256`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExternalRef {
|
||||
/// Which sibling product this reference points at.
|
||||
pub system: ExternalSystem,
|
||||
/// The sibling product's project identifier, if known.
|
||||
pub project_id: Option<String>,
|
||||
/// Content digest of the subject artifact (firmware sha256), if known.
|
||||
pub subject_sha256: Option<String>,
|
||||
/// Reconciliation status: `linked` | `reconciled` | `unavailable`.
|
||||
#[serde(default)]
|
||||
pub status: String,
|
||||
/// Opaque, offline-verifiable entitlement grant (e.g. tramiton's signed
|
||||
/// `LicenseGrant`), if the tenant provided one.
|
||||
pub license_grant: Option<String>,
|
||||
/// When evidence was last reconciled from this system.
|
||||
#[serde(default, with = "super::serde_helpers::opt_bson_datetime")]
|
||||
pub last_reconciled_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl ExternalRef {
|
||||
/// A freshly linked (not yet reconciled) reference to a sibling system.
|
||||
pub fn linked(system: ExternalSystem) -> Self {
|
||||
Self {
|
||||
system,
|
||||
project_id: None,
|
||||
subject_sha256: None,
|
||||
status: "linked".to_string(),
|
||||
license_grant: None,
|
||||
last_reconciled_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A regulatory / standards framework a target must comply with. Drives which
|
||||
/// controls the mapping engine pulls from the [`crate::traits::ControlsProvider`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ComplianceFramework {
|
||||
/// EU Cyber Resilience Act.
|
||||
Cra,
|
||||
/// IEC 62443 (industrial automation & control systems security).
|
||||
Iec62443,
|
||||
/// EU General Data Protection Regulation.
|
||||
Gdpr,
|
||||
/// SOC 2.
|
||||
Soc2,
|
||||
/// ISO/IEC 27001.
|
||||
Iso27001,
|
||||
/// EU Radio Equipment Directive (RED) cybersecurity articles.
|
||||
RedDirective,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ComplianceFramework {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Cra => write!(f, "cra"),
|
||||
Self::Iec62443 => write!(f, "iec_62443"),
|
||||
Self::Gdpr => write!(f, "gdpr"),
|
||||
Self::Soc2 => write!(f, "soc2"),
|
||||
Self::Iso27001 => write!(f, "iso_27001"),
|
||||
Self::RedDirective => write!(f, "red_directive"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The compliance scope of a target: which frameworks apply and, optionally, the
|
||||
/// jurisdiction. Captured at onboarding (with per-target-type defaults from
|
||||
/// [`default_compliance_profile`]).
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ComplianceProfile {
|
||||
/// Applicable frameworks.
|
||||
#[serde(default)]
|
||||
pub frameworks: Vec<ComplianceFramework>,
|
||||
/// Free-form jurisdiction (e.g. `eu`, `us`, `de`).
|
||||
pub jurisdiction: Option<String>,
|
||||
}
|
||||
|
||||
/// The sensible default compliance scope for a target type. Firmware / PLC /
|
||||
/// embedded default to CRA + IEC 62443; software defaults to GDPR + SOC 2.
|
||||
pub fn default_compliance_profile(target_type: TargetType) -> ComplianceProfile {
|
||||
use ComplianceFramework::{Cra, Gdpr, Iec62443, Soc2};
|
||||
let frameworks = match target_type {
|
||||
TargetType::PlcSps
|
||||
| TargetType::FirmwareBareMetal
|
||||
| TargetType::FirmwareRtos
|
||||
| TargetType::EmbeddedLinuxYocto => vec![Cra, Iec62443],
|
||||
TargetType::WebApp | TargetType::BackendService => vec![Gdpr, Soc2],
|
||||
TargetType::DesktopApp | TargetType::AndroidApp | TargetType::IosApp => {
|
||||
vec![Gdpr, Cra]
|
||||
}
|
||||
};
|
||||
ComplianceProfile {
|
||||
frameworks,
|
||||
jurisdiction: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A target onboarded for scanning: the unified replacement for the legacy
|
||||
/// `TrackedRepository` (SAST) and `DastTarget` (DAST) records.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -434,6 +562,12 @@ pub struct OnboardedTarget {
|
||||
/// How this target should be scanned.
|
||||
#[serde(default)]
|
||||
pub scan_config: TargetScanConfig,
|
||||
/// The compliance scope (applicable frameworks / jurisdiction).
|
||||
#[serde(default)]
|
||||
pub compliance_profile: ComplianceProfile,
|
||||
/// Links to sibling products (tramiton, ...) holding reconcilable evidence.
|
||||
#[serde(default)]
|
||||
pub external_refs: Vec<ExternalRef>,
|
||||
/// Cron schedule for recurring scans, if any.
|
||||
pub scan_schedule: Option<String>,
|
||||
/// Whether inbound webhooks are enabled for this target.
|
||||
@@ -472,6 +606,8 @@ impl OnboardedTarget {
|
||||
artifacts: Vec::new(),
|
||||
classification: None,
|
||||
scan_config: TargetScanConfig::default(),
|
||||
compliance_profile: default_compliance_profile(target_type),
|
||||
external_refs: Vec::new(),
|
||||
scan_schedule: None,
|
||||
webhook_enabled: false,
|
||||
webhook_secret: Some(webhook_secret),
|
||||
@@ -584,4 +720,34 @@ mod tests {
|
||||
let b = Artifact::firmware_image("fw.bin");
|
||||
assert_ne!(a.id, b.id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firmware_default_profile_is_cra_and_62443() {
|
||||
let p = default_compliance_profile(TargetType::FirmwareBareMetal);
|
||||
assert!(p.frameworks.contains(&ComplianceFramework::Cra));
|
||||
assert!(p.frameworks.contains(&ComplianceFramework::Iec62443));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webapp_default_profile_is_gdpr_and_soc2() {
|
||||
let p = default_compliance_profile(TargetType::WebApp);
|
||||
assert!(p.frameworks.contains(&ComplianceFramework::Gdpr));
|
||||
assert!(p.frameworks.contains(&ComplianceFramework::Soc2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_target_gets_default_profile_and_no_external_refs() {
|
||||
let t = OnboardedTarget::new("fw".to_string(), TargetType::FirmwareRtos);
|
||||
assert!(!t.compliance_profile.frameworks.is_empty());
|
||||
assert!(t.external_refs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_ref_linked_defaults() {
|
||||
let r = ExternalRef::linked(ExternalSystem::Tramiton);
|
||||
assert_eq!(r.system, ExternalSystem::Tramiton);
|
||||
assert_eq!(r.status, "linked");
|
||||
assert!(r.project_id.is_none());
|
||||
assert!(r.last_reconciled_at.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user