Skip to main content

keyhog_core/spec/
semantic.rs

1use serde::{Deserialize, Serialize};
2
3/// Syntactic role of the bytes captured as a detector credential.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
5#[serde(rename_all = "kebab-case")]
6pub enum CaptureSemanticRole {
7    /// Compatibility state for an omitted declaration; carries no syntax proof.
8    #[default]
9    Unknown,
10    /// Credential bytes captured from an assignment value.
11    AssignmentValue,
12    /// Standalone opaque token bytes.
13    Token,
14    /// Credential bytes captured from a structured envelope.
15    CredentialEnvelope,
16    /// A complete private-key block.
17    PrivateKeyBlock,
18    /// A complete credential-bearing connection string.
19    ConnectionString,
20    /// Credential bytes from URL user information.
21    UrlUserinfo,
22    /// Credential bytes from a protocol header value.
23    HeaderValue,
24    /// Credential bytes from a command argument.
25    CommandArgumentValue,
26}
27
28impl CaptureSemanticRole {
29    /// Return the stable detector TOML spelling.
30    pub const fn as_str(self) -> &'static str {
31        match self {
32            Self::Unknown => "unknown",
33            Self::AssignmentValue => "assignment-value",
34            Self::Token => "token",
35            Self::CredentialEnvelope => "credential-envelope",
36            Self::PrivateKeyBlock => "private-key-block",
37            Self::ConnectionString => "connection-string",
38            Self::UrlUserinfo => "url-userinfo",
39            Self::HeaderValue => "header-value",
40            Self::CommandArgumentValue => "command-argument-value",
41        }
42    }
43
44    /// Whether this role carries no semantic proof.
45    pub const fn is_unknown(&self) -> bool {
46        matches!(self, Self::Unknown)
47    }
48}
49
50/// Strength and kind of the detector anchor surrounding a capture.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
52#[serde(rename_all = "kebab-case")]
53pub enum AnchorSemanticRole {
54    /// No anchor semantics are declared.
55    #[default]
56    Unknown,
57    /// An exact credential key anchors the capture.
58    ExactKey,
59    /// A vendor-distinctive literal prefix anchors the capture.
60    DistinctivePrefix,
61    /// A structured credential envelope anchors the capture.
62    StructuredEnvelope,
63    /// Required companion evidence anchors the capture.
64    CompanionBound,
65    /// Only weak contextual text anchors the capture.
66    WeakContext,
67    /// No surrounding anchor is required.
68    Unanchored,
69}
70
71impl AnchorSemanticRole {
72    /// Return the stable detector TOML spelling.
73    pub const fn as_str(self) -> &'static str {
74        match self {
75            Self::Unknown => "unknown",
76            Self::ExactKey => "exact-key",
77            Self::DistinctivePrefix => "distinctive-prefix",
78            Self::StructuredEnvelope => "structured-envelope",
79            Self::CompanionBound => "companion-bound",
80            Self::WeakContext => "weak-context",
81            Self::Unanchored => "unanchored",
82        }
83    }
84
85    /// Whether this role carries no anchor proof.
86    pub const fn is_unknown(&self) -> bool {
87        matches!(self, Self::Unknown)
88    }
89}
90
91/// Candidate-bounded semantic classification of the source containing a match.
92#[repr(u8)]
93#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
94#[serde(rename_all = "kebab-case")]
95pub enum SemanticSourceRole {
96    /// Value of a parsed structured assignment.
97    StructuredAssignmentValue,
98    /// Value of an environment assignment.
99    EnvironmentAssignmentValue,
100    /// Literal string value in source code.
101    StringLiteral,
102    /// Value passed to a command argument.
103    CommandArgumentValue,
104    /// Declaration of a command option rather than its runtime value.
105    CommandOptionDeclaration,
106    /// Value of a protocol header.
107    HeaderValue,
108    /// Authority or user-information field of a URL.
109    UrlAuthorityUserinfo,
110    /// Credential field within a connection string.
111    ConnectionString,
112    /// Standalone opaque token.
113    StandaloneToken,
114    /// Value contained in a PEM block.
115    PemBlock,
116    /// Regex, scanner rule, or grammar definition.
117    RegexRuleDefinition,
118    /// Identifier, type, or member name.
119    IdentifierTypeMemberName,
120    /// Prose or documentation content.
121    ProseDocumentation,
122    /// Test or example fixture content.
123    TestFixture,
124    /// Generated or vendored material.
125    GeneratedVendorMaterial,
126    /// Unsupported, ambiguous, or unparsed context; carries no source-role proof.
127    Unknown,
128}
129
130impl SemanticSourceRole {
131    /// Return the stable detector TOML spelling.
132    pub const fn as_str(self) -> &'static str {
133        match self {
134            Self::StructuredAssignmentValue => "structured-assignment-value",
135            Self::EnvironmentAssignmentValue => "environment-assignment-value",
136            Self::StringLiteral => "string-literal",
137            Self::CommandArgumentValue => "command-argument-value",
138            Self::CommandOptionDeclaration => "command-option-declaration",
139            Self::HeaderValue => "header-value",
140            Self::UrlAuthorityUserinfo => "url-authority-userinfo",
141            Self::ConnectionString => "connection-string",
142            Self::StandaloneToken => "standalone-token",
143            Self::PemBlock => "pem-block",
144            Self::RegexRuleDefinition => "regex-rule-definition",
145            Self::IdentifierTypeMemberName => "identifier-type-member-name",
146            Self::ProseDocumentation => "prose-documentation",
147            Self::TestFixture => "test-fixture",
148            Self::GeneratedVendorMaterial => "generated-vendor-material",
149            Self::Unknown => "unknown",
150        }
151    }
152}
153
154/// Typed semantic proof named by a detector policy.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
156#[serde(rename_all = "kebab-case")]
157pub enum RequiredSemanticEvidence {
158    /// Intrinsic checksum validation.
159    Checksum,
160    /// Required detector companion evidence.
161    RequiredCompanion,
162    /// Paired private-key companion evidence.
163    PrivateKeyCompanion,
164    /// Structural grammar validation.
165    StructuralGrammar,
166    /// Successful live credential verification.
167    LiveVerification,
168}
169
170impl RequiredSemanticEvidence {
171    /// Return the stable detector TOML spelling.
172    pub const fn as_str(self) -> &'static str {
173        match self {
174            Self::Checksum => "checksum",
175            Self::RequiredCompanion => "required-companion",
176            Self::PrivateKeyCompanion => "private-key-companion",
177            Self::StructuralGrammar => "structural-grammar",
178            Self::LiveVerification => "live-verification",
179        }
180    }
181}
182
183/// Named synthetic false-positive class carried by detector test evidence.
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
185#[serde(rename_all = "kebab-case")]
186pub enum DetectorHardNegativeClass {
187    /// A valid-looking token placed across an invalid lexical boundary.
188    Boundary,
189    /// An identifier, type, or member name that resembles a credential.
190    Identifier,
191    /// Prose that contains credential-shaped vocabulary or bytes.
192    Prose,
193    /// A regex, scanner rule, or grammar literal.
194    RegexLiteral,
195    /// A nearby provider or token prefix that the detector does not own.
196    SiblingPrefix,
197}
198
199impl DetectorHardNegativeClass {
200    /// Complete class registry in declaration order.
201    pub const ALL: &'static [Self] = &[
202        Self::Boundary,
203        Self::Identifier,
204        Self::Prose,
205        Self::RegexLiteral,
206        Self::SiblingPrefix,
207    ];
208
209    /// Return the stable detector TOML spelling.
210    pub const fn as_str(self) -> &'static str {
211        match self {
212            Self::Boundary => "boundary",
213            Self::Identifier => "identifier",
214            Self::Prose => "prose",
215            Self::RegexLiteral => "regex-literal",
216            Self::SiblingPrefix => "sibling-prefix",
217        }
218    }
219}
220
221/// Canonical detector semantic policy copied into compiled and packed plans.
222///
223/// The policy participates in execution identity. Current scan admission does
224/// not consume its declarations.
225#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
226#[serde(deny_unknown_fields)]
227pub struct DetectorSemanticPolicySpec {
228    /// Syntactic role of the captured credential bytes.
229    #[serde(default)]
230    pub capture_role: CaptureSemanticRole,
231    /// Strength and kind of the detector anchor.
232    #[serde(default)]
233    pub anchor_role: AnchorSemanticRole,
234    /// Detector-owned source roles.
235    #[serde(default, skip_serializing_if = "Vec::is_empty")]
236    pub allowed_source_roles: Vec<SemanticSourceRole>,
237    /// Detector-owned evidence requirements.
238    #[serde(default, skip_serializing_if = "Vec::is_empty")]
239    pub required_evidence: Vec<RequiredSemanticEvidence>,
240}
241
242impl DetectorSemanticPolicySpec {
243    /// Whether the declaration carries every typed field required for verdict
244    /// enforcement.
245    pub fn is_enforcement_capable(&self) -> bool {
246        self.capture_role != CaptureSemanticRole::Unknown
247            && self.anchor_role != AnchorSemanticRole::Unknown
248            && !self.allowed_source_roles.is_empty()
249            && self
250                .allowed_source_roles
251                .iter()
252                .all(|role| *role != SemanticSourceRole::Unknown)
253    }
254}