Skip to main content

taudit_core/
finding.rs

1use crate::graph::NodeId;
2use crate::propagation::PropagationPath;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum Severity {
8    Critical,
9    High,
10    Medium,
11    Low,
12    Info,
13}
14
15impl Severity {
16    fn rank(self) -> u8 {
17        match self {
18            Severity::Critical => 0,
19            Severity::High => 1,
20            Severity::Medium => 2,
21            Severity::Low => 3,
22            Severity::Info => 4,
23        }
24    }
25}
26
27impl Ord for Severity {
28    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
29        self.rank().cmp(&other.rank())
30    }
31}
32
33impl PartialOrd for Severity {
34    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
35        Some(self.cmp(other))
36    }
37}
38
39/// MVP categories (1-5) are derivable from pipeline YAML alone.
40/// Stretch categories (6-9) need heuristics or metadata enrichment.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum FindingCategory {
44    // MVP
45    AuthorityPropagation,
46    OverPrivilegedIdentity,
47    UnpinnedAction,
48    UntrustedWithAuthority,
49    ArtifactBoundaryCrossing,
50    // Stretch — implemented
51    FloatingImage,
52    LongLivedCredential,
53    /// Credential written to disk by a step (e.g. `persistCredentials: true` on a checkout).
54    /// Disk-persisted credentials are accessible to all subsequent steps and any process
55    /// with filesystem access, unlike runtime-only `HasAccessTo` authority.
56    PersistedCredential,
57    /// Dangerous trigger type (pull_request_target / pr) combined with secret/identity access.
58    TriggerContextMismatch,
59    /// Authority (secret/identity) flows into an opaque external workflow via DelegatesTo.
60    CrossWorkflowAuthorityChain,
61    /// Circular DelegatesTo chain — workflow calls itself transitively.
62    AuthorityCycle,
63    /// Privileged workflow (OIDC/broad identity) with no provenance attestation step.
64    UpliftWithoutAttestation,
65    /// Step writes to the environment gate ($GITHUB_ENV, pipeline variables) — authority can propagate.
66    SelfMutatingPipeline,
67    /// PR-triggered pipeline checks out the repository — attacker-controlled fork code lands on the runner.
68    CheckoutSelfPrExposure,
69    /// ADO variable group consumed by a PR-triggered job, crossing trust boundary.
70    VariableGroupInPrJob,
71    /// Self-hosted agent pool used in a PR-triggered job that also checks out the repository.
72    SelfHostedPoolPrHijack,
73    /// Broad-scope ADO service connection reachable from a PR-triggered job without OIDC.
74    ServiceConnectionScopeMismatch,
75    /// ADO `resources.repositories[]` entry referenced by an `extends:`,
76    /// `template: x@alias`, or `checkout: alias` consumer resolves with no
77    /// `ref:` (default branch) or a mutable branch ref (`refs/heads/<name>`).
78    /// Whoever owns that branch can inject steps into the consuming pipeline.
79    TemplateExtendsUnpinnedBranch,
80    /// Pipeline step uses an Azure VM remote-exec primitive (Set-AzVMExtension /
81    /// CustomScriptExtension, Invoke-AzVMRunCommand, az vm run-command, az vm extension set)
82    /// where the executed command line interpolates a pipeline secret or a SAS token —
83    /// pipeline-to-VM lateral movement primitive logged in plaintext to the VM and ARM.
84    VmRemoteExecViaPipelineSecret,
85    /// A SAS token freshly minted in-pipeline is interpolated into a CLI argument
86    /// (commandToExecute / scriptArguments / --arguments / -ArgumentList) instead of
87    /// passed via env var or stdin — argv ends up in /proc/*/cmdline, ETW, ARM status.
88    ShortLivedSasInCommandLine,
89    /// Pipeline secret value assigned to a shell variable inside an inline
90    /// script (`export VAR=$(SECRET)`, `$X = "$(SECRET)"`). Once the value
91    /// transits a shell variable, ADO's `$(SECRET)` log mask no longer
92    /// applies — transcripts (`Start-Transcript`, `bash -x`, terraform debug
93    /// logs) print the cleartext.
94    SecretToInlineScriptEnvExport,
95    /// Pipeline secret value written to a file under the agent workspace
96    /// (`$(System.DefaultWorkingDirectory)`, `$(Build.SourcesDirectory)`,
97    /// or relative paths) without `secureFile` task or chmod 600. The file
98    /// persists in the agent workspace and is uploaded by
99    /// `PublishPipelineArtifact` and crawlable by later steps.
100    SecretMaterialisedToWorkspaceFile,
101    /// PowerShell pulls a Key Vault secret with `-AsPlainText` (or
102    /// `ConvertFrom-SecureString -AsPlainText`, or older
103    /// `.SecretValueText` syntax) into a non-`SecureString` variable. The
104    /// value never traverses the ADO variable-group boundary, so verbose
105    /// Az/PS logging and error stack traces print the credential.
106    ///
107    /// Rule id is `keyvault_secret_to_plaintext` (single token "keyvault")
108    /// rather than the snake_case derivation `key_vault_…` — matches the
109    /// docs filename and the convention used in the corpus evidence.
110    #[serde(rename = "keyvault_secret_to_plaintext")]
111    KeyVaultSecretToPlaintext,
112    /// `terraform apply -auto-approve` against a production-named service connection
113    /// without an environment approval gate.
114    TerraformAutoApproveInProd,
115    /// `AzureCLI@2` task with `addSpnToEnvironment: true` AND an inline script —
116    /// the script can launder federated SPN/OIDC tokens into pipeline variables.
117    AddSpnWithInlineScript,
118    /// A `type: string` pipeline parameter (no `values:` allowlist) is interpolated
119    /// via `${{ parameters.X }}` into an inline shell/PowerShell script body —
120    /// shell injection vector for anyone with "queue build".
121    ParameterInterpolationIntoShell,
122    // Reserved — requires ADO/GH API enrichment beyond pipeline YAML
123    /// Requires runtime network telemetry or policy enrichment — not detectable from YAML alone.
124    #[doc(hidden)]
125    EgressBlindspot,
126    /// Requires external audit-sink configuration data — not detectable from YAML alone.
127    #[doc(hidden)]
128    MissingAuditTrail,
129}
130
131/// Routing: scope findings -> TsafeRemediation; isolation findings -> CellosRemediation.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133#[serde(tag = "type", rename_all = "snake_case")]
134pub enum Recommendation {
135    TsafeRemediation {
136        command: String,
137        explanation: String,
138    },
139    CellosRemediation {
140        reason: String,
141        spec_hint: String,
142    },
143    PinAction {
144        current: String,
145        pinned: String,
146    },
147    ReducePermissions {
148        current: String,
149        minimum: String,
150    },
151    FederateIdentity {
152        static_secret: String,
153        oidc_provider: String,
154    },
155    Manual {
156        action: String,
157    },
158}
159
160/// A finding is a concrete, actionable authority issue.
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct Finding {
163    pub severity: Severity,
164    pub category: FindingCategory,
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub path: Option<PropagationPath>,
167    pub nodes_involved: Vec<NodeId>,
168    pub message: String,
169    pub recommendation: Recommendation,
170}