cargo-allow 0.1.11

Source-tree exception ledger and policy scanner for Rust repositories.
use allow_core::AllowConfig;

use allow_report::{
    DiffFindingChange, DiffLedgerMovementSummary, DiffMovementCounts, DiffPolicyChange,
    DiffPostureDeltaCounts,
};

pub struct FindingRowBundle {
    pub change: allow_diff::FindingPostureChange,
    pub subject: String,
    pub ledger_id: String,
    pub lane: String,
}

pub struct PolicyRowBundle {
    pub change: allow_diff::PolicyChange,
    pub subject: String,
    pub ledger_id: String,
    pub lane: Option<String>,
}

pub struct DiffLedgerContext<'a> {
    pub base_cfg: &'a AllowConfig,
    pub head_cfg: &'a AllowConfig,
    pub finding_changes: &'a [allow_diff::FindingPostureChange],
    pub policy_changes: &'a [allow_diff::PolicyChange],
}

impl<'a> DiffLedgerContext<'a> {
    pub fn new(
        base_cfg: &'a AllowConfig,
        head_cfg: &'a AllowConfig,
        finding_changes: &'a [allow_diff::FindingPostureChange],
        policy_changes: &'a [allow_diff::PolicyChange],
    ) -> Self {
        Self {
            base_cfg,
            head_cfg,
            finding_changes,
            policy_changes,
        }
    }

    pub fn ledger_movement_summary(&self) -> DiffLedgerMovementSummary {
        ledger_movement_summary_from_diff(allow_diff::diff_ledger_movement_summary(
            self.base_cfg,
            self.head_cfg,
            self.finding_changes,
            self.policy_changes,
        ))
    }
}

pub fn ledger_movement_summary_from_diff(
    summary: allow_diff::DiffLedgerMovementSummary,
) -> DiffLedgerMovementSummary {
    DiffLedgerMovementSummary {
        movement: DiffMovementCounts {
            introduced: summary.movement.introduced,
            retained: summary.movement.retained,
            removed: summary.movement.removed,
        },
        posture_delta: DiffPostureDeltaCounts {
            improved: summary.posture_delta.improved,
            worsened: summary.posture_delta.worsened,
            review_required: summary.posture_delta.review_required,
            unchanged: summary.posture_delta.unchanged,
        },
    }
}

pub fn finding_row_bundles(
    changes: &[allow_diff::FindingPostureChange],
    head_cfg: &AllowConfig,
) -> Vec<FindingRowBundle> {
    let ledger_id = allow_diff::entry_ledger_id(head_cfg);
    changes
        .iter()
        .map(|change| FindingRowBundle {
            subject: allow_diff::finding_posture_subject(change),
            lane: default_lane_for_finding_kind(head_cfg, &change.finding_kind),
            ledger_id: ledger_id.clone(),
            change: change.clone(),
        })
        .collect()
}

pub fn policy_row_bundles(
    changes: &[allow_diff::PolicyChange],
    head_cfg: &AllowConfig,
) -> Vec<PolicyRowBundle> {
    let ledger_id = allow_diff::policy_change_ledger_id(head_cfg);
    changes
        .iter()
        .map(|change| PolicyRowBundle {
            subject: allow_diff::policy_change_subject(change),
            lane: allow_diff::policy_change_lane(head_cfg, change)
                .or_else(|| default_lane_for_policy_change(head_cfg, change)),
            ledger_id: ledger_id.clone(),
            change: change.clone(),
        })
        .collect()
}

pub fn finding_change_rows(bundles: &[FindingRowBundle]) -> Vec<DiffFindingChange<'_>> {
    bundles
        .iter()
        .map(|bundle| {
            let class = allow_diff::classify_finding_posture_change(&bundle.change);
            DiffFindingChange {
                change: bundle.change.kind.as_str(),
                movement: class.movement.field_name(),
                posture_delta: class.posture_delta.field_name(),
                changed_in_diff: class.changed_in_diff,
                subject: Some(bundle.subject.as_str()),
                allow_id: None,
                ledger_id: Some(bundle.ledger_id.as_str()),
                lane: Some(bundle.lane.as_str()),
                key: &bundle.change.key,
                kind: &bundle.change.finding_kind,
                family: bundle.change.family.as_deref(),
                path: &bundle.change.path,
                line: bundle.change.line,
                column: bundle.change.column,
                source_package: bundle.change.source_package.as_deref(),
                identity: Some(&bundle.change.identity),
            }
        })
        .collect()
}

pub fn policy_change_rows(bundles: &[PolicyRowBundle]) -> Vec<DiffPolicyChange<'_>> {
    bundles
        .iter()
        .map(|bundle| {
            let class = allow_diff::classify_policy_change(&bundle.change);
            DiffPolicyChange {
                severity: bundle.change.severity.as_str(),
                movement: class.movement.field_name(),
                posture_delta: class.posture_delta.field_name(),
                changed_in_diff: class.changed_in_diff,
                subject: Some(bundle.subject.as_str()),
                allow_id: &bundle.change.allow_id,
                ledger_id: Some(bundle.ledger_id.as_str()),
                lane: bundle.lane.as_deref(),
                kind: bundle.change.kind.as_str(),
                message: &bundle.change.message,
                exception_identity: bundle.change.exception_identity.as_ref().map(|identity| {
                    allow_report::DiffExceptionIdentityChange {
                        field: identity.field.as_str(),
                        before: identity.before.as_deref(),
                        after: identity.after.as_deref(),
                    }
                }),
                selector_identity: bundle.change.selector_identity.as_ref().map(|identity| {
                    allow_report::DiffSelectorIdentityChange {
                        changed_fields: &identity.changed_fields,
                    }
                }),
                selector_precision: bundle.change.selector_precision.as_ref().map(|selector| {
                    allow_report::DiffSelectorPrecisionChange {
                        before: selector.before,
                        after: selector.after,
                        removed_fields: &selector.removed_fields,
                        added_fields: &selector.added_fields,
                    }
                }),
                scope: bundle
                    .change
                    .scope
                    .as_ref()
                    .map(|scope| allow_report::DiffScopeChange {
                        field: scope.field.as_str(),
                        before: scope.before.as_deref(),
                        after: scope.after.as_deref(),
                    }),
                occurrence_limit: bundle.change.occurrence_limit.as_ref().map(|limit| {
                    allow_report::DiffOccurrenceLimitChange {
                        before: limit.before,
                        after: limit.after,
                    }
                }),
                lifecycle: bundle.change.lifecycle.as_ref().map(|lifecycle| {
                    allow_report::DiffLifecycleChange {
                        field: lifecycle.field.as_str(),
                        before: lifecycle.before.as_deref(),
                        after: lifecycle.after.as_deref(),
                    }
                }),
                evidence: bundle.change.evidence.as_ref().map(|evidence| {
                    allow_report::DiffEvidenceChange {
                        field: evidence.field.as_str(),
                        removed: &evidence.removed,
                        added: &evidence.added,
                    }
                }),
                metadata: bundle.change.metadata.as_ref().map(|metadata| {
                    allow_report::DiffMetadataChange {
                        field: metadata.field.as_str(),
                        before: metadata.before.as_deref(),
                        after: metadata.after.as_deref(),
                    }
                }),
                requirement: bundle.change.requirement.as_ref().map(|requirement| {
                    allow_report::DiffRequirementChange {
                        field: requirement.field.as_str(),
                        before: requirement.before,
                        after: requirement.after,
                    }
                }),
                policy_status: bundle.change.policy_status.as_ref().map(|policy_status| {
                    allow_report::DiffPolicyStatusChange {
                        before: policy_status.before.as_deref(),
                        after: policy_status.after.as_deref(),
                    }
                }),
            }
        })
        .collect()
}
fn default_lane_for_finding_kind(cfg: &AllowConfig, kind: &str) -> String {
    if cfg.lanes.contains_key(kind) {
        kind.to_string()
    } else {
        "source-exception".to_string()
    }
}

fn default_lane_for_policy_change(
    cfg: &AllowConfig,
    change: &allow_diff::PolicyChange,
) -> Option<String> {
    if change.allow_id.starts_with("requirements.")
        || change.allow_id.starts_with("policy.")
        || change.allow_id.starts_with("workspace.")
    {
        None
    } else if let Some(entry) = cfg.allow.iter().find(|entry| entry.id == change.allow_id) {
        Some(allow_diff::entry_lane(cfg, entry))
    } else {
        Some("source-exception".to_string())
    }
}