Skip to main content

termivar_scanner/
control_reference_mapping.rs

1//! Bounded, offline control-reference mapping for composed assessment items.
2//!
3//! The built-in catalogue deliberately contains only reviewed identifiers,
4//! bibliographic metadata, and original Termivar mapping rationales. Mapping an
5//! item supplies technical review context; it is not a control assessment,
6//! compliance determination, certification, or legal conclusion.
7
8use std::collections::BTreeSet;
9
10use thiserror::Error;
11
12use crate::web_runtime::{AssessmentBasis, AssessmentItem};
13
14/// Stable audit schema written by the reporting layer.
15pub const CONTROL_REFERENCE_MAPPING_AUDIT_SCHEMA: &str =
16    "security.control-reference-mapping-audit/v1";
17/// Stable mapping-policy identity.
18pub const CONTROL_REFERENCE_MAPPING_POLICY_ID: &str = "termivar.control-reference-mapping/v1";
19/// Stable identity of the reviewed, built-in reference catalogue.
20pub const CONTROL_REFERENCE_MAPPING_CATALOGUE_ID: &str = "termivar.reviewed-control-references";
21/// Revision of the built-in catalogue used by this source tree.
22pub const CONTROL_REFERENCE_MAPPING_CATALOGUE_REVISION: &str = "2026-09-20.1";
23/// Date on which the primary source metadata was reviewed.
24pub const CONTROL_REFERENCE_SOURCE_VERIFIED_AT: &str = "2026-09-20";
25
26/// Maximum assessment items accepted by one mapping pass.
27pub const MAX_CONTROL_REFERENCE_MAPPING_ITEMS: usize = 4_096;
28/// Maximum retained mapped-item relationships.
29pub const MAX_CONTROL_REFERENCE_MAPPING_RELATIONSHIPS: usize = 4_096;
30/// Maximum control-reference links retained across one audit.
31pub const MAX_CONTROL_REFERENCE_MAPPING_LINKS: usize = 16_384;
32/// Maximum control-reference links retained for one assessment item.
33pub const MAX_CONTROL_REFERENCE_LINKS_PER_ITEM: usize = 8;
34
35const OWASP_SOURCE_ID: &str = "owasp-top-10-2025";
36const PCI_SOURCE_ID: &str = "pci-dss-4.0.1";
37const ISO_SOURCE_ID: &str = "iso-iec-27001-2022-amd-1-2024";
38const KVKK_LAW_SOURCE_ID: &str = "kvkk-law-6698-article-12";
39const KVKK_GUIDE_SOURCE_ID: &str = "kvkk-guide-72-2025-04";
40
41const OWASP_A01: &str = "A01:2025";
42const OWASP_A02: &str = "A02:2025";
43const OWASP_A05: &str = "A05:2025";
44const KVKK_ARTICLE_12: &str = "6698/Madde-12";
45const KVKK_GUIDE_72: &str = "KVKK-Rehber-72/2025-04";
46
47/// Authority class for a reviewed external reference source.
48#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
49pub enum ControlReferenceAuthorityKind {
50    /// Open community security project.
51    CommunitySecurityProject,
52    /// Industry standards organization.
53    IndustryStandardsBody,
54    /// International standards organization.
55    InternationalStandardsOrganization,
56    /// Statutory text or regulator publication.
57    StatutoryAuthority,
58}
59
60impl ControlReferenceAuthorityKind {
61    /// Stable wire token.
62    pub const fn as_str(self) -> &'static str {
63        match self {
64            Self::CommunitySecurityProject => "community_security_project",
65            Self::IndustryStandardsBody => "industry_standards_body",
66            Self::InternationalStandardsOrganization => "international_standards_organization",
67            Self::StatutoryAuthority => "statutory_authority",
68        }
69    }
70}
71
72/// Availability of control-level mappings from one source.
73#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
74pub enum ControlReferenceMappingAvailability {
75    /// Reviewed identifiers and short titles can be mapped with attribution.
76    ReviewedIdentifiersAndTitles,
77    /// Only bibliographic source metadata is retained because rights review is deferred.
78    BibliographicOnlyRightsDeferred,
79    /// The source is used only as relevant technical context.
80    RelevantTechnicalContextOnly,
81}
82
83impl ControlReferenceMappingAvailability {
84    /// Stable wire token.
85    pub const fn as_str(self) -> &'static str {
86        match self {
87            Self::ReviewedIdentifiersAndTitles => "reviewed_identifiers_and_titles",
88            Self::BibliographicOnlyRightsDeferred => "bibliographic_only_rights_deferred",
89            Self::RelevantTechnicalContextOnly => "relevant_technical_context_only",
90        }
91    }
92}
93
94/// Rights posture for the metadata retained from one source.
95#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
96pub enum ControlReferenceRightsBasis {
97    /// Identifiers and short titles retained with source attribution.
98    IdentifiersAndTitlesWithAttribution,
99    /// Only bibliographic metadata and an official source link are retained.
100    BibliographicMetadataOnly,
101    /// Only an official legal/regulator reference and original rationale are retained.
102    OfficialReferenceOnly,
103}
104
105impl ControlReferenceRightsBasis {
106    /// Stable wire token.
107    pub const fn as_str(self) -> &'static str {
108        match self {
109            Self::IdentifiersAndTitlesWithAttribution => "identifiers_and_titles_with_attribution",
110            Self::BibliographicMetadataOnly => "bibliographic_metadata_only",
111            Self::OfficialReferenceOnly => "official_reference_only",
112        }
113    }
114}
115
116/// Typed assessment-item authority required by a mapping rule.
117#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
118pub enum ControlReferenceItemBasisClass {
119    /// Passive or structural observation.
120    Observation,
121    /// Matched control/candidate differential.
122    Differential,
123    /// Native verifier result.
124    Verifier,
125}
126
127impl ControlReferenceItemBasisClass {
128    /// Stable wire token.
129    pub const fn as_str(self) -> &'static str {
130        match self {
131            Self::Observation => "observation",
132            Self::Differential => "differential",
133            Self::Verifier => "verifier",
134        }
135    }
136
137    fn from_basis(basis: &AssessmentBasis) -> Self {
138        match basis {
139            AssessmentBasis::Observation(_) => Self::Observation,
140            AssessmentBasis::Differential(_) => Self::Differential,
141            AssessmentBasis::Verifier(_) => Self::Verifier,
142        }
143    }
144}
145
146/// Exact matching inputs used by one mapping rule.
147#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
148pub enum ControlReferenceMappingBasis {
149    /// Exact capability, exact CWE, and exact assessment basis.
150    ExactCapabilityCweAndAssessmentBasis,
151    /// Exact capability, absent CWE, and exact assessment basis.
152    ExactCapabilityAndAssessmentBasis,
153}
154
155impl ControlReferenceMappingBasis {
156    /// Stable wire token.
157    pub const fn as_str(self) -> &'static str {
158        match self {
159            Self::ExactCapabilityCweAndAssessmentBasis => {
160                "exact_capability_cwe_and_assessment_basis"
161            },
162            Self::ExactCapabilityAndAssessmentBasis => "exact_capability_and_assessment_basis",
163        }
164    }
165}
166
167/// Applicability qualification attached to a control reference.
168#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
169pub enum ControlReferenceApplicability {
170    /// The reference is relevant only under the stated technical conditions.
171    ConditionalTechnicalContext,
172    /// Organizational/legal applicability was not established by the scan.
173    ApplicabilityUnestablished,
174}
175
176impl ControlReferenceApplicability {
177    /// Stable wire token.
178    pub const fn as_str(self) -> &'static str {
179        match self {
180            Self::ConditionalTechnicalContext => "conditional_technical_context",
181            Self::ApplicabilityUnestablished => "applicability_unestablished",
182        }
183    }
184}
185
186/// Assurance of the mapping source and relationship.
187#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
188pub enum ControlReferenceMappingAssurance {
189    /// Identifier/edition checked against the named official primary source.
190    ReviewedPrimarySourceIdentifier,
191    /// Relevant technical context only; applicability is not established.
192    RelevantTechnicalContext,
193}
194
195impl ControlReferenceMappingAssurance {
196    /// Stable wire token.
197    pub const fn as_str(self) -> &'static str {
198        match self {
199            Self::ReviewedPrimarySourceIdentifier => "reviewed_primary_source_identifier",
200            Self::RelevantTechnicalContext => "relevant_technical_context",
201        }
202    }
203}
204
205/// Whether external source retrieval occurred while producing an audit.
206#[derive(Clone, Copy, Debug, Eq, PartialEq)]
207pub enum ControlReferenceSourceRetrievalState {
208    /// Mapping used only the built-in reviewed catalogue.
209    NotPerformed,
210}
211
212impl ControlReferenceSourceRetrievalState {
213    /// Stable wire token.
214    pub const fn as_str(self) -> &'static str {
215        match self {
216            Self::NotPerformed => "not_performed",
217        }
218    }
219}
220
221/// Whether an actual framework control assessment was performed.
222#[derive(Clone, Copy, Debug, Eq, PartialEq)]
223pub enum ControlAssessmentState {
224    /// No control assessment was performed.
225    NotPerformed,
226}
227
228impl ControlAssessmentState {
229    /// Stable wire token.
230    pub const fn as_str(self) -> &'static str {
231        match self {
232            Self::NotPerformed => "not_performed",
233        }
234    }
235}
236
237/// State for compliance, certification, legal, and source-authentication claims.
238#[derive(Clone, Copy, Debug, Eq, PartialEq)]
239pub enum ControlReferenceClaimState {
240    /// The proposition was not established by this mapping operation.
241    NotEstablished,
242}
243
244impl ControlReferenceClaimState {
245    /// Stable wire token.
246    pub const fn as_str(self) -> &'static str {
247        match self {
248            Self::NotEstablished => "not_established",
249        }
250    }
251}
252
253/// Reviewed metadata for one reference source.
254#[derive(Clone, Copy, Debug, Eq, PartialEq)]
255pub struct ControlReferenceSource {
256    source_id: &'static str,
257    framework_id: &'static str,
258    edition: &'static str,
259    revision_or_amendment: Option<&'static str>,
260    authority_kind: ControlReferenceAuthorityKind,
261    source_url: &'static str,
262    source_verified_at: &'static str,
263    mapping_availability: ControlReferenceMappingAvailability,
264    rights_basis: ControlReferenceRightsBasis,
265}
266
267impl ControlReferenceSource {
268    /// Stable source identity.
269    pub const fn source_id(&self) -> &'static str {
270        self.source_id
271    }
272
273    /// Stable framework or legal-source identity.
274    pub const fn framework_id(&self) -> &'static str {
275        self.framework_id
276    }
277
278    /// Explicit source edition.
279    pub const fn edition(&self) -> &'static str {
280        self.edition
281    }
282
283    /// Explicit revision or amendment, when applicable.
284    pub const fn revision_or_amendment(&self) -> Option<&'static str> {
285        self.revision_or_amendment
286    }
287
288    /// Source authority class.
289    pub const fn authority_kind(&self) -> ControlReferenceAuthorityKind {
290        self.authority_kind
291    }
292
293    /// Official source URL; it is bibliographic data and is never fetched here.
294    pub const fn source_url(&self) -> &'static str {
295        self.source_url
296    }
297
298    /// Date on which the source metadata was reviewed.
299    pub const fn source_verified_at(&self) -> &'static str {
300        self.source_verified_at
301    }
302
303    /// Control-level mapping availability.
304    pub const fn mapping_availability(&self) -> ControlReferenceMappingAvailability {
305        self.mapping_availability
306    }
307
308    /// Rights posture governing retained source metadata.
309    pub const fn rights_basis(&self) -> ControlReferenceRightsBasis {
310        self.rights_basis
311    }
312}
313
314const REVIEWED_SOURCES: [ControlReferenceSource; 5] = [
315    ControlReferenceSource {
316        source_id: OWASP_SOURCE_ID,
317        framework_id: "owasp-top-10",
318        edition: "2025",
319        revision_or_amendment: None,
320        authority_kind: ControlReferenceAuthorityKind::CommunitySecurityProject,
321        source_url: "https://top10.owasp.org/2025/0x00_2025-Introduction/",
322        source_verified_at: CONTROL_REFERENCE_SOURCE_VERIFIED_AT,
323        mapping_availability: ControlReferenceMappingAvailability::ReviewedIdentifiersAndTitles,
324        rights_basis: ControlReferenceRightsBasis::IdentifiersAndTitlesWithAttribution,
325    },
326    ControlReferenceSource {
327        source_id: PCI_SOURCE_ID,
328        framework_id: "pci-dss",
329        edition: "4.0.1",
330        revision_or_amendment: Some("published-2024-06-11"),
331        authority_kind: ControlReferenceAuthorityKind::IndustryStandardsBody,
332        source_url: "https://blog.pcisecuritystandards.org/just-published-pci-dss-v4-0-1",
333        source_verified_at: CONTROL_REFERENCE_SOURCE_VERIFIED_AT,
334        mapping_availability:
335            ControlReferenceMappingAvailability::BibliographicOnlyRightsDeferred,
336        rights_basis: ControlReferenceRightsBasis::BibliographicMetadataOnly,
337    },
338    ControlReferenceSource {
339        source_id: ISO_SOURCE_ID,
340        framework_id: "iso-iec-27001",
341        edition: "2022",
342        revision_or_amendment: Some("Amd-1:2024"),
343        authority_kind: ControlReferenceAuthorityKind::InternationalStandardsOrganization,
344        source_url: "https://www.iso.org/standard/27001",
345        source_verified_at: CONTROL_REFERENCE_SOURCE_VERIFIED_AT,
346        mapping_availability:
347            ControlReferenceMappingAvailability::BibliographicOnlyRightsDeferred,
348        rights_basis: ControlReferenceRightsBasis::BibliographicMetadataOnly,
349    },
350    ControlReferenceSource {
351        source_id: KVKK_LAW_SOURCE_ID,
352        framework_id: "kvkk-law-6698",
353        edition: "6698",
354        revision_or_amendment: Some("Madde-12"),
355        authority_kind: ControlReferenceAuthorityKind::StatutoryAuthority,
356        source_url: "https://www.kvkk.gov.tr/Icerik/2040/Veri-Guvenligine-Iliskin-Yukumlulukler",
357        source_verified_at: CONTROL_REFERENCE_SOURCE_VERIFIED_AT,
358        mapping_availability:
359            ControlReferenceMappingAvailability::RelevantTechnicalContextOnly,
360        rights_basis: ControlReferenceRightsBasis::OfficialReferenceOnly,
361    },
362    ControlReferenceSource {
363        source_id: KVKK_GUIDE_SOURCE_ID,
364        framework_id: "kvkk-personal-data-security-guide",
365        edition: "2025-04",
366        revision_or_amendment: Some("KVKK-Yayinlari-No-72"),
367        authority_kind: ControlReferenceAuthorityKind::StatutoryAuthority,
368        source_url: "https://kvkk.gov.tr/SharedFolderServer/CMSFiles/7512d0d4-f345-41cb-bc5b-8d5cf125e3a1.pdf",
369        source_verified_at: CONTROL_REFERENCE_SOURCE_VERIFIED_AT,
370        mapping_availability:
371            ControlReferenceMappingAvailability::RelevantTechnicalContextOnly,
372        rights_basis: ControlReferenceRightsBasis::OfficialReferenceOnly,
373    },
374];
375
376/// One source-qualified control-reference link.
377#[derive(Clone, Debug, Eq, PartialEq)]
378pub struct ControlReferenceLink {
379    rule_id: &'static str,
380    source_id: &'static str,
381    framework_id: &'static str,
382    control_reference: &'static str,
383    control_title: Option<&'static str>,
384    mapping_basis: ControlReferenceMappingBasis,
385    applicability: ControlReferenceApplicability,
386    applicability_condition: &'static str,
387    assurance: ControlReferenceMappingAssurance,
388    original_mapping_rationale: &'static str,
389}
390
391impl ControlReferenceLink {
392    /// Stable mapping-rule identity.
393    pub const fn rule_id(&self) -> &'static str {
394        self.rule_id
395    }
396
397    /// Source identity for this reference.
398    pub const fn source_id(&self) -> &'static str {
399        self.source_id
400    }
401
402    /// Framework or legal-source identity.
403    pub const fn framework_id(&self) -> &'static str {
404        self.framework_id
405    }
406
407    /// Edition-qualified control reference.
408    pub const fn control_reference(&self) -> &'static str {
409        self.control_reference
410    }
411
412    /// Short title retained only when the reviewed source permits it.
413    pub const fn control_title(&self) -> Option<&'static str> {
414        self.control_title
415    }
416
417    /// Exact mapping inputs used by this rule.
418    pub const fn mapping_basis(&self) -> ControlReferenceMappingBasis {
419        self.mapping_basis
420    }
421
422    /// Applicability classification.
423    pub const fn applicability(&self) -> ControlReferenceApplicability {
424        self.applicability
425    }
426
427    /// Original, bounded applicability condition.
428    pub const fn applicability_condition(&self) -> &'static str {
429        self.applicability_condition
430    }
431
432    /// Mapping assurance classification.
433    pub const fn assurance(&self) -> ControlReferenceMappingAssurance {
434        self.assurance
435    }
436
437    /// Original Termivar rationale; no standards prose is copied here.
438    pub const fn original_mapping_rationale(&self) -> &'static str {
439        self.original_mapping_rationale
440    }
441}
442
443/// One assessment item and its bounded control-reference links.
444#[derive(Clone, Debug, Eq, PartialEq)]
445pub struct ControlReferenceRelationship {
446    item_fingerprint: String,
447    capability_id: String,
448    cwe: Option<String>,
449    item_basis: ControlReferenceItemBasisClass,
450    references: Vec<ControlReferenceLink>,
451}
452
453impl ControlReferenceRelationship {
454    /// Stable assessment-item fingerprint.
455    pub fn item_fingerprint(&self) -> &str {
456        &self.item_fingerprint
457    }
458
459    /// Exact mapped capability identity.
460    pub fn capability_id(&self) -> &str {
461        &self.capability_id
462    }
463
464    /// Exact mapped CWE, when the rule requires one.
465    pub fn cwe(&self) -> Option<&str> {
466        self.cwe.as_deref()
467    }
468
469    /// Exact mapped assessment-item basis.
470    pub const fn item_basis(&self) -> ControlReferenceItemBasisClass {
471        self.item_basis
472    }
473
474    /// Stable, duplicate-free control-reference links.
475    pub fn references(&self) -> &[ControlReferenceLink] {
476        &self.references
477    }
478}
479
480/// External-activity accounting for the pure mapping operation.
481#[derive(Clone, Copy, Debug, Eq, PartialEq)]
482pub struct ControlReferenceExternalActivity {
483    target_request_count: u64,
484    provider_request_count: u64,
485    source_retrieval: ControlReferenceSourceRetrievalState,
486}
487
488impl ControlReferenceExternalActivity {
489    /// Target requests introduced by mapping.
490    pub const fn target_request_count(&self) -> u64 {
491        self.target_request_count
492    }
493
494    /// Provider requests introduced by mapping.
495    pub const fn provider_request_count(&self) -> u64 {
496        self.provider_request_count
497    }
498
499    /// External source-retrieval state.
500    pub const fn source_retrieval(&self) -> ControlReferenceSourceRetrievalState {
501        self.source_retrieval
502    }
503}
504
505/// Explicit claim limits for a control-reference audit.
506#[derive(Clone, Copy, Debug, Eq, PartialEq)]
507pub struct ControlReferenceClaimLimits {
508    control_assessment: ControlAssessmentState,
509    compliance: ControlReferenceClaimState,
510    certification: ControlReferenceClaimState,
511    legal_conclusion: ControlReferenceClaimState,
512    source_authentication: ControlReferenceClaimState,
513}
514
515impl ControlReferenceClaimLimits {
516    /// Whether an actual framework control assessment occurred.
517    pub const fn control_assessment(&self) -> ControlAssessmentState {
518        self.control_assessment
519    }
520
521    /// Compliance-conclusion state.
522    pub const fn compliance(&self) -> ControlReferenceClaimState {
523        self.compliance
524    }
525
526    /// Certification-conclusion state.
527    pub const fn certification(&self) -> ControlReferenceClaimState {
528        self.certification
529    }
530
531    /// Legal-conclusion state.
532    pub const fn legal_conclusion(&self) -> ControlReferenceClaimState {
533        self.legal_conclusion
534    }
535
536    /// Source-authentication state.
537    pub const fn source_authentication(&self) -> ControlReferenceClaimState {
538        self.source_authentication
539    }
540}
541
542/// Complete bounded output of an offline mapping pass.
543#[derive(Clone, Debug, Eq, PartialEq)]
544pub struct ControlReferenceMappingAudit {
545    selected: bool,
546    sources: Vec<ControlReferenceSource>,
547    relationships: Vec<ControlReferenceRelationship>,
548    considered_item_count: usize,
549    mapped_item_count: usize,
550    unmapped_item_count: usize,
551    reference_link_count: usize,
552    omitted_item_count: usize,
553    omitted_reference_link_count: usize,
554    external_activity: ControlReferenceExternalActivity,
555    claim_limits: ControlReferenceClaimLimits,
556}
557
558impl ControlReferenceMappingAudit {
559    /// Stable audit schema.
560    pub const fn schema(&self) -> &'static str {
561        CONTROL_REFERENCE_MAPPING_AUDIT_SCHEMA
562    }
563
564    /// Stable mapping policy.
565    pub const fn policy_id(&self) -> &'static str {
566        CONTROL_REFERENCE_MAPPING_POLICY_ID
567    }
568
569    /// Built-in catalogue identity.
570    pub const fn catalogue_id(&self) -> &'static str {
571        CONTROL_REFERENCE_MAPPING_CATALOGUE_ID
572    }
573
574    /// Built-in catalogue revision.
575    pub const fn catalogue_revision(&self) -> &'static str {
576        CONTROL_REFERENCE_MAPPING_CATALOGUE_REVISION
577    }
578
579    /// The mapping option was explicitly selected.
580    pub const fn selected(&self) -> bool {
581        self.selected
582    }
583
584    /// Reviewed source metadata, including bibliographic-only sources.
585    pub fn sources(&self) -> &[ControlReferenceSource] {
586        &self.sources
587    }
588
589    /// Mapped item relationships, ordered by item fingerprint.
590    pub fn relationships(&self) -> &[ControlReferenceRelationship] {
591        &self.relationships
592    }
593
594    /// Input assessment items considered.
595    pub const fn considered_item_count(&self) -> usize {
596        self.considered_item_count
597    }
598
599    /// Distinct input items with at least one reference link.
600    pub const fn mapped_item_count(&self) -> usize {
601        self.mapped_item_count
602    }
603
604    /// Distinct input items with no applicable exact rule.
605    pub const fn unmapped_item_count(&self) -> usize {
606        self.unmapped_item_count
607    }
608
609    /// Number of mapped-item relationship rows.
610    pub fn relationship_count(&self) -> usize {
611        self.relationships.len()
612    }
613
614    /// Total retained reference links.
615    pub const fn reference_link_count(&self) -> usize {
616        self.reference_link_count
617    }
618
619    /// Items omitted from this bounded operation.
620    pub const fn omitted_item_count(&self) -> usize {
621        self.omitted_item_count
622    }
623
624    /// Links omitted from this bounded operation.
625    pub const fn omitted_reference_link_count(&self) -> usize {
626        self.omitted_reference_link_count
627    }
628
629    /// Number of reviewed source metadata rows.
630    pub fn source_count(&self) -> usize {
631        self.sources.len()
632    }
633
634    /// External-activity accounting.
635    pub const fn external_activity(&self) -> ControlReferenceExternalActivity {
636        self.external_activity
637    }
638
639    /// Explicit limits on conclusions supported by this audit.
640    pub const fn claim_limits(&self) -> ControlReferenceClaimLimits {
641        self.claim_limits
642    }
643}
644
645/// Failure to produce a complete, bounded control-reference audit.
646#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
647pub enum ControlReferenceMappingError {
648    /// The assessment contains more items than the mapping bound.
649    #[error("control-reference mapping item limit exceeded")]
650    TooManyItems,
651    /// Two assessment items have the same stable fingerprint.
652    #[error("duplicate assessment-item fingerprint")]
653    DuplicateItemFingerprint,
654    /// The mapped-item relationship bound was exceeded.
655    #[error("control-reference mapping relationship limit exceeded")]
656    TooManyRelationships,
657    /// One item maps to too many control references.
658    #[error("per-item control-reference limit exceeded")]
659    TooManyReferencesPerItem,
660    /// The aggregate control-reference link bound was exceeded.
661    #[error("control-reference mapping link limit exceeded")]
662    TooManyReferenceLinks,
663    /// A mapping rule produced the same control-reference link twice.
664    #[error("duplicate control-reference link")]
665    DuplicateControlReference,
666    /// A mapping counter overflowed.
667    #[error("control-reference mapping count overflow")]
668    CountOverflow,
669}
670
671#[derive(Clone, Copy)]
672struct MappingItemView<'a> {
673    fingerprint: &'a str,
674    capability_id: &'a str,
675    cwe: Option<&'a str>,
676    basis: ControlReferenceItemBasisClass,
677}
678
679impl<'a> From<&'a AssessmentItem> for MappingItemView<'a> {
680    fn from(item: &'a AssessmentItem) -> Self {
681        Self {
682            fingerprint: item.fingerprint(),
683            capability_id: item.capability_id(),
684            cwe: item.cwe(),
685            basis: ControlReferenceItemBasisClass::from_basis(item.basis()),
686        }
687    }
688}
689
690#[derive(Clone, Copy)]
691struct MappingRule {
692    capability_id: &'static str,
693    cwe: Option<&'static str>,
694    basis: ControlReferenceItemBasisClass,
695    rule_id: &'static str,
696    source_id: &'static str,
697    framework_id: &'static str,
698    control_reference: &'static str,
699    control_title: Option<&'static str>,
700    mapping_basis: ControlReferenceMappingBasis,
701    applicability: ControlReferenceApplicability,
702    applicability_condition: &'static str,
703    assurance: ControlReferenceMappingAssurance,
704    original_mapping_rationale: &'static str,
705}
706
707const RULES: [MappingRule; 24] = [
708    MappingRule {
709        capability_id: "web.review.sql.structural-differential@1",
710        cwe: Some("CWE-89"),
711        basis: ControlReferenceItemBasisClass::Differential,
712        rule_id: "termivar.mapping.sql-cwe-89-to-owasp-a05-2025@1",
713        source_id: OWASP_SOURCE_ID,
714        framework_id: "owasp-top-10",
715        control_reference: OWASP_A05,
716        control_title: Some("Injection"),
717        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityCweAndAssessmentBasis,
718        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
719        applicability_condition: "The mapped item is the exact bounded SQL structural differential with CWE-89 metadata.",
720        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
721        original_mapping_rationale: "The exact differential is relevant to review of injection-resistant query construction; it does not establish database access or exploitation.",
722    },
723    MappingRule {
724        capability_id: "web.review.xss.structural-boundary@1",
725        cwe: Some("CWE-79"),
726        basis: ControlReferenceItemBasisClass::Differential,
727        rule_id: "termivar.mapping.xss-cwe-79-to-owasp-a05-2025@1",
728        source_id: OWASP_SOURCE_ID,
729        framework_id: "owasp-top-10",
730        control_reference: OWASP_A05,
731        control_title: Some("Injection"),
732        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityCweAndAssessmentBasis,
733        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
734        applicability_condition: "The mapped item is the exact non-executing XSS structural differential with CWE-79 metadata.",
735        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
736        original_mapping_rationale: "The exact parser-visible differential is relevant to injection review; script execution and exploitability remain untested.",
737    },
738    MappingRule {
739        capability_id: "web.review.ssti.structural-evaluation@1",
740        cwe: Some("CWE-1336"),
741        basis: ControlReferenceItemBasisClass::Differential,
742        rule_id: "termivar.mapping.ssti-cwe-1336-to-owasp-a05-2025@1",
743        source_id: OWASP_SOURCE_ID,
744        framework_id: "owasp-top-10",
745        control_reference: OWASP_A05,
746        control_title: Some("Injection"),
747        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityCweAndAssessmentBasis,
748        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
749        applicability_condition: "The mapped item is the exact arithmetic template differential with CWE-1336 metadata.",
750        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
751        original_mapping_rationale: "The exact template-expression differential is relevant to injection review; engine identity, code execution, and impact remain untested.",
752    },
753    MappingRule {
754        capability_id: "web.review.cors.credentialed-external-origin@1",
755        cwe: Some("CWE-942"),
756        basis: ControlReferenceItemBasisClass::Differential,
757        rule_id: "termivar.mapping.cors-cwe-942-to-owasp-a01-2025@1",
758        source_id: OWASP_SOURCE_ID,
759        framework_id: "owasp-top-10",
760        control_reference: OWASP_A01,
761        control_title: Some("Broken Access Control"),
762        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityCweAndAssessmentBasis,
763        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
764        applicability_condition: "The mapped item is the exact credentialed external-origin differential with CWE-942 metadata.",
765        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
766        original_mapping_rationale: "The exact cross-origin policy differential is relevant to access-control review; browser exploitability and data access remain untested.",
767    },
768    MappingRule {
769        capability_id: "web.review.redirect.candidate-specific-external@1",
770        cwe: Some("CWE-601"),
771        basis: ControlReferenceItemBasisClass::Differential,
772        rule_id: "termivar.mapping.redirect-cwe-601-to-owasp-a01-2025@1",
773        source_id: OWASP_SOURCE_ID,
774        framework_id: "owasp-top-10",
775        control_reference: OWASP_A01,
776        control_title: Some("Broken Access Control"),
777        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityCweAndAssessmentBasis,
778        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
779        applicability_condition: "The mapped item is the exact candidate-specific external redirect differential with CWE-601 metadata.",
780        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
781        original_mapping_rationale: "The exact redirect differential is relevant to destination-control review; no redirect was followed and no downstream impact was tested.",
782    },
783    MappingRule {
784        capability_id: "authorization.resource-cross-principal-equivalence@1",
785        cwe: None,
786        basis: ControlReferenceItemBasisClass::Differential,
787        rule_id: "termivar.mapping.authorization-cross-principal-to-owasp-a01-2025@1",
788        source_id: OWASP_SOURCE_ID,
789        framework_id: "owasp-top-10",
790        control_reference: OWASP_A01,
791        control_title: Some("Broken Access Control"),
792        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
793        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
794        applicability_condition: "The mapped item is the exact operator-policy-bound cross-principal equivalence differential and carries no CWE claim.",
795        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
796        original_mapping_rationale: "The exact cross-principal differential is relevant to access-control review; operator expectations and resource equivalence do not alone establish unauthorized access.",
797    },
798    MappingRule {
799        capability_id: "ssrf.oast-repeated-outbound-interaction@1",
800        cwe: Some("CWE-918"),
801        basis: ControlReferenceItemBasisClass::Differential,
802        rule_id: "termivar.mapping.ssrf-cwe-918-to-owasp-a01-2025@1",
803        source_id: OWASP_SOURCE_ID,
804        framework_id: "owasp-top-10",
805        control_reference: OWASP_A01,
806        control_title: Some("Broken Access Control"),
807        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityCweAndAssessmentBasis,
808        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
809        applicability_condition: "The mapped item is the exact repeated OAST interaction differential with CWE-918 metadata.",
810        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
811        original_mapping_rationale: "OWASP Top 10:2025 places SSRF within A01; the mapped interaction remains review-level and does not establish exploitability or business impact.",
812    },
813    MappingRule {
814        capability_id: "web.passive.hsts.missing@1",
815        cwe: None,
816        basis: ControlReferenceItemBasisClass::Observation,
817        rule_id: "termivar.mapping.hsts-missing-to-owasp-a02-2025@1",
818        source_id: OWASP_SOURCE_ID,
819        framework_id: "owasp-top-10",
820        control_reference: OWASP_A02,
821        control_title: Some("Security Misconfiguration"),
822        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
823        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
824        applicability_condition: "The mapped item is the exact complete-response HSTS-missing observation on an eligible HTTPS response.",
825        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
826        original_mapping_rationale: "The exact missing-header observation is relevant to transport-policy configuration review; it is not an organization-wide control assessment.",
827    },
828    MappingRule {
829        capability_id: "web.passive.hsts.max-age-zero@1",
830        cwe: None,
831        basis: ControlReferenceItemBasisClass::Observation,
832        rule_id: "termivar.mapping.hsts-max-age-zero-to-owasp-a02-2025@1",
833        source_id: OWASP_SOURCE_ID,
834        framework_id: "owasp-top-10",
835        control_reference: OWASP_A02,
836        control_title: Some("Security Misconfiguration"),
837        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
838        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
839        applicability_condition: "The mapped item is the exact complete-response zero-max-age HSTS observation.",
840        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
841        original_mapping_rationale: "The exact zero-retention observation is relevant to transport-policy configuration review; intended deployment policy remains operator context.",
842    },
843    MappingRule {
844        capability_id: "web.passive.hsts.nonconformant@1",
845        cwe: None,
846        basis: ControlReferenceItemBasisClass::Observation,
847        rule_id: "termivar.mapping.hsts-nonconformant-to-owasp-a02-2025@1",
848        source_id: OWASP_SOURCE_ID,
849        framework_id: "owasp-top-10",
850        control_reference: OWASP_A02,
851        control_title: Some("Security Misconfiguration"),
852        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
853        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
854        applicability_condition: "The mapped item is the exact complete-response nonconformant HSTS observation.",
855        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
856        original_mapping_rationale: "The exact malformed-policy observation is relevant to transport-policy configuration review; it does not establish broader configuration compliance.",
857    },
858    MappingRule {
859        capability_id: "web.passive.csp.missing@1",
860        cwe: None,
861        basis: ControlReferenceItemBasisClass::Observation,
862        rule_id: "termivar.mapping.csp-missing-to-owasp-a02-2025@1",
863        source_id: OWASP_SOURCE_ID,
864        framework_id: "owasp-top-10",
865        control_reference: OWASP_A02,
866        control_title: Some("Security Misconfiguration"),
867        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
868        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
869        applicability_condition: "The mapped item is the exact complete-response CSP-missing observation.",
870        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
871        original_mapping_rationale: "The exact missing-policy observation is relevant to browser content-policy configuration review; it is not proof of executable injection or a control assessment.",
872    },
873    MappingRule {
874        capability_id: "web.passive.csp.nonconformant@1",
875        cwe: None,
876        basis: ControlReferenceItemBasisClass::Observation,
877        rule_id: "termivar.mapping.csp-nonconformant-to-owasp-a02-2025@1",
878        source_id: OWASP_SOURCE_ID,
879        framework_id: "owasp-top-10",
880        control_reference: OWASP_A02,
881        control_title: Some("Security Misconfiguration"),
882        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
883        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
884        applicability_condition: "The mapped item is the exact complete-response nonconformant CSP observation.",
885        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
886        original_mapping_rationale: "The exact malformed-policy observation is relevant to browser content-policy configuration review; effective browser behavior and wider control coverage remain untested.",
887    },
888    MappingRule {
889        capability_id: "web.passive.csp.unsafe-inline-declared@1",
890        cwe: None,
891        basis: ControlReferenceItemBasisClass::Observation,
892        rule_id: "termivar.mapping.csp-unsafe-inline-to-owasp-a02-2025@1",
893        source_id: OWASP_SOURCE_ID,
894        framework_id: "owasp-top-10",
895        control_reference: OWASP_A02,
896        control_title: Some("Security Misconfiguration"),
897        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
898        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
899        applicability_condition: "The mapped item is the exact complete-response CSP unsafe-inline declaration observation.",
900        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
901        original_mapping_rationale: "The exact directive observation is relevant to browser content-policy configuration review; it does not establish an injection path or script execution.",
902    },
903    MappingRule {
904        capability_id: "web.passive.csp.unsafe-eval-declared@1",
905        cwe: None,
906        basis: ControlReferenceItemBasisClass::Observation,
907        rule_id: "termivar.mapping.csp-unsafe-eval-to-owasp-a02-2025@1",
908        source_id: OWASP_SOURCE_ID,
909        framework_id: "owasp-top-10",
910        control_reference: OWASP_A02,
911        control_title: Some("Security Misconfiguration"),
912        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
913        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
914        applicability_condition: "The mapped item is the exact complete-response CSP unsafe-eval declaration observation.",
915        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
916        original_mapping_rationale: "The exact directive observation is relevant to browser content-policy configuration review; it does not establish attacker-controlled evaluation or impact.",
917    },
918    MappingRule {
919        capability_id: "web.passive.x-content-type-options.missing@1",
920        cwe: None,
921        basis: ControlReferenceItemBasisClass::Observation,
922        rule_id: "termivar.mapping.x-content-type-options-missing-to-owasp-a02-2025@1",
923        source_id: OWASP_SOURCE_ID,
924        framework_id: "owasp-top-10",
925        control_reference: OWASP_A02,
926        control_title: Some("Security Misconfiguration"),
927        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
928        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
929        applicability_condition: "The mapped item is the exact complete-response X-Content-Type-Options-missing observation.",
930        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
931        original_mapping_rationale: "The exact missing-header observation is relevant to response content-type configuration review; browser exploitation and organizational control coverage remain untested.",
932    },
933    MappingRule {
934        capability_id: "web.passive.x-content-type-options.not-nosniff@1",
935        cwe: None,
936        basis: ControlReferenceItemBasisClass::Observation,
937        rule_id: "termivar.mapping.x-content-type-options-not-nosniff-to-owasp-a02-2025@1",
938        source_id: OWASP_SOURCE_ID,
939        framework_id: "owasp-top-10",
940        control_reference: OWASP_A02,
941        control_title: Some("Security Misconfiguration"),
942        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
943        applicability: ControlReferenceApplicability::ConditionalTechnicalContext,
944        applicability_condition: "The mapped item is the exact complete-response X-Content-Type-Options not-nosniff observation.",
945        assurance: ControlReferenceMappingAssurance::ReviewedPrimarySourceIdentifier,
946        original_mapping_rationale: "The exact header-value observation is relevant to response content-type configuration review; browser exploitation and wider compliance remain untested.",
947    },
948    MappingRule {
949        capability_id: "exposure.response-private-key-material@1",
950        cwe: None,
951        basis: ControlReferenceItemBasisClass::Observation,
952        rule_id: "termivar.mapping.private-key-exposure-to-kvkk-article-12@1",
953        source_id: KVKK_LAW_SOURCE_ID,
954        framework_id: "kvkk-law-6698",
955        control_reference: KVKK_ARTICLE_12,
956        control_title: None,
957        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
958        applicability: ControlReferenceApplicability::ApplicabilityUnestablished,
959        applicability_condition: "The exact redacted response observation may be relevant only if applicable personal-data processing and organizational scope are independently established.",
960        assurance: ControlReferenceMappingAssurance::RelevantTechnicalContext,
961        original_mapping_rationale: "Returned private-key-like material can be relevant technical security context, but ownership, validity, personal-data scope, and legal applicability were not established.",
962    },
963    MappingRule {
964        capability_id: "exposure.response-aws-access-key-pair@1",
965        cwe: None,
966        basis: ControlReferenceItemBasisClass::Observation,
967        rule_id: "termivar.mapping.aws-key-pair-exposure-to-kvkk-article-12@1",
968        source_id: KVKK_LAW_SOURCE_ID,
969        framework_id: "kvkk-law-6698",
970        control_reference: KVKK_ARTICLE_12,
971        control_title: None,
972        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
973        applicability: ControlReferenceApplicability::ApplicabilityUnestablished,
974        applicability_condition: "The exact redacted paired-credential observation may be relevant only if applicable personal-data processing and organizational scope are independently established.",
975        assurance: ControlReferenceMappingAssurance::RelevantTechnicalContext,
976        original_mapping_rationale: "Returned paired credential material can be relevant technical security context, but validity, permissions, ownership, personal-data scope, and legal applicability were not established.",
977    },
978    MappingRule {
979        capability_id: "exposure.response-stripe-live-secret@1",
980        cwe: None,
981        basis: ControlReferenceItemBasisClass::Observation,
982        rule_id: "termivar.mapping.stripe-live-secret-exposure-to-kvkk-article-12@1",
983        source_id: KVKK_LAW_SOURCE_ID,
984        framework_id: "kvkk-law-6698",
985        control_reference: KVKK_ARTICLE_12,
986        control_title: None,
987        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
988        applicability: ControlReferenceApplicability::ApplicabilityUnestablished,
989        applicability_condition: "The exact redacted provider-secret observation may be relevant only if applicable personal-data processing and organizational scope are independently established.",
990        assurance: ControlReferenceMappingAssurance::RelevantTechnicalContext,
991        original_mapping_rationale: "Returned live-secret-like material can be relevant technical security context, but validity, permissions, ownership, personal-data scope, and legal applicability were not established.",
992    },
993    MappingRule {
994        capability_id: "exposure.response-bearer-authorization@1",
995        cwe: None,
996        basis: ControlReferenceItemBasisClass::Observation,
997        rule_id: "termivar.mapping.bearer-authorization-exposure-to-kvkk-article-12@1",
998        source_id: KVKK_LAW_SOURCE_ID,
999        framework_id: "kvkk-law-6698",
1000        control_reference: KVKK_ARTICLE_12,
1001        control_title: None,
1002        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
1003        applicability: ControlReferenceApplicability::ApplicabilityUnestablished,
1004        applicability_condition: "The exact redacted bearer-material observation may be relevant only if applicable personal-data processing and organizational scope are independently established.",
1005        assurance: ControlReferenceMappingAssurance::RelevantTechnicalContext,
1006        original_mapping_rationale: "Returned bearer-material-like content can be relevant technical security context, but validity, permissions, ownership, personal-data scope, and legal applicability were not established.",
1007    },
1008    MappingRule {
1009        capability_id: "exposure.response-private-key-material@1",
1010        cwe: None,
1011        basis: ControlReferenceItemBasisClass::Observation,
1012        rule_id: "termivar.mapping.private-key-exposure-to-kvkk-guide-72-2025@1",
1013        source_id: KVKK_GUIDE_SOURCE_ID,
1014        framework_id: "kvkk-personal-data-security-guide",
1015        control_reference: KVKK_GUIDE_72,
1016        control_title: None,
1017        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
1018        applicability: ControlReferenceApplicability::ApplicabilityUnestablished,
1019        applicability_condition: "The exact redacted response observation is relevant technical context only if applicable personal-data processing and organizational scope are independently established.",
1020        assurance: ControlReferenceMappingAssurance::RelevantTechnicalContext,
1021        original_mapping_rationale: "The official KVKK security guide is relevant context for safeguarding data-processing environments; this observation does not establish personal-data scope, organizational control failure, or legal noncompliance.",
1022    },
1023    MappingRule {
1024        capability_id: "exposure.response-aws-access-key-pair@1",
1025        cwe: None,
1026        basis: ControlReferenceItemBasisClass::Observation,
1027        rule_id: "termivar.mapping.aws-key-pair-exposure-to-kvkk-guide-72-2025@1",
1028        source_id: KVKK_GUIDE_SOURCE_ID,
1029        framework_id: "kvkk-personal-data-security-guide",
1030        control_reference: KVKK_GUIDE_72,
1031        control_title: None,
1032        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
1033        applicability: ControlReferenceApplicability::ApplicabilityUnestablished,
1034        applicability_condition: "The exact redacted paired-credential observation is relevant technical context only if applicable personal-data processing and organizational scope are independently established.",
1035        assurance: ControlReferenceMappingAssurance::RelevantTechnicalContext,
1036        original_mapping_rationale: "The official KVKK security guide is relevant context for safeguarding data-processing environments; this paired-credential observation does not establish validity, personal-data scope, organizational control failure, or legal noncompliance.",
1037    },
1038    MappingRule {
1039        capability_id: "exposure.response-stripe-live-secret@1",
1040        cwe: None,
1041        basis: ControlReferenceItemBasisClass::Observation,
1042        rule_id: "termivar.mapping.stripe-live-secret-exposure-to-kvkk-guide-72-2025@1",
1043        source_id: KVKK_GUIDE_SOURCE_ID,
1044        framework_id: "kvkk-personal-data-security-guide",
1045        control_reference: KVKK_GUIDE_72,
1046        control_title: None,
1047        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
1048        applicability: ControlReferenceApplicability::ApplicabilityUnestablished,
1049        applicability_condition: "The exact redacted provider-secret observation is relevant technical context only if applicable personal-data processing and organizational scope are independently established.",
1050        assurance: ControlReferenceMappingAssurance::RelevantTechnicalContext,
1051        original_mapping_rationale: "The official KVKK security guide is relevant context for safeguarding data-processing environments; this provider-secret observation does not establish validity, personal-data scope, organizational control failure, or legal noncompliance.",
1052    },
1053    MappingRule {
1054        capability_id: "exposure.response-bearer-authorization@1",
1055        cwe: None,
1056        basis: ControlReferenceItemBasisClass::Observation,
1057        rule_id: "termivar.mapping.bearer-authorization-exposure-to-kvkk-guide-72-2025@1",
1058        source_id: KVKK_GUIDE_SOURCE_ID,
1059        framework_id: "kvkk-personal-data-security-guide",
1060        control_reference: KVKK_GUIDE_72,
1061        control_title: None,
1062        mapping_basis: ControlReferenceMappingBasis::ExactCapabilityAndAssessmentBasis,
1063        applicability: ControlReferenceApplicability::ApplicabilityUnestablished,
1064        applicability_condition: "The exact redacted bearer-material observation is relevant technical context only if applicable personal-data processing and organizational scope are independently established.",
1065        assurance: ControlReferenceMappingAssurance::RelevantTechnicalContext,
1066        original_mapping_rationale: "The official KVKK security guide is relevant context for safeguarding data-processing environments; this bearer-material observation does not establish validity, personal-data scope, organizational control failure, or legal noncompliance.",
1067    },
1068];
1069
1070/// Maps composed assessment items through the closed, reviewed reference catalogue.
1071///
1072/// The operation is pure and transport-free. It neither mutates assessment items
1073/// nor changes their disposition, severity, evidence, or claim authority.
1074pub fn map_control_references(
1075    items: &[AssessmentItem],
1076) -> Result<ControlReferenceMappingAudit, ControlReferenceMappingError> {
1077    let views = items.iter().map(MappingItemView::from).collect::<Vec<_>>();
1078    map_item_views(&views)
1079}
1080
1081fn map_item_views(
1082    items: &[MappingItemView<'_>],
1083) -> Result<ControlReferenceMappingAudit, ControlReferenceMappingError> {
1084    if items.len() > MAX_CONTROL_REFERENCE_MAPPING_ITEMS {
1085        return Err(ControlReferenceMappingError::TooManyItems);
1086    }
1087
1088    let mut fingerprints = BTreeSet::new();
1089    for item in items {
1090        if !fingerprints.insert(item.fingerprint) {
1091            return Err(ControlReferenceMappingError::DuplicateItemFingerprint);
1092        }
1093    }
1094
1095    let mut relationships = Vec::new();
1096    let mut reference_link_count = 0_usize;
1097    for item in items {
1098        let mut references = RULES
1099            .iter()
1100            .filter(|rule| {
1101                rule.capability_id == item.capability_id
1102                    && rule.cwe == item.cwe
1103                    && rule.basis == item.basis
1104            })
1105            .map(rule_to_link)
1106            .collect::<Vec<_>>();
1107        if references.is_empty() {
1108            continue;
1109        }
1110        references.sort_by(|left, right| {
1111            left.rule_id
1112                .cmp(right.rule_id)
1113                .then(left.source_id.cmp(right.source_id))
1114                .then(left.control_reference.cmp(right.control_reference))
1115        });
1116        validate_reference_links(&references)?;
1117        reference_link_count = reference_link_count
1118            .checked_add(references.len())
1119            .ok_or(ControlReferenceMappingError::CountOverflow)?;
1120        if reference_link_count > MAX_CONTROL_REFERENCE_MAPPING_LINKS {
1121            return Err(ControlReferenceMappingError::TooManyReferenceLinks);
1122        }
1123        relationships.push(ControlReferenceRelationship {
1124            item_fingerprint: item.fingerprint.to_owned(),
1125            capability_id: item.capability_id.to_owned(),
1126            cwe: item.cwe.map(str::to_owned),
1127            item_basis: item.basis,
1128            references,
1129        });
1130        if relationships.len() > MAX_CONTROL_REFERENCE_MAPPING_RELATIONSHIPS {
1131            return Err(ControlReferenceMappingError::TooManyRelationships);
1132        }
1133    }
1134    relationships.sort_by(|left, right| {
1135        left.item_fingerprint
1136            .cmp(&right.item_fingerprint)
1137            .then(left.capability_id.cmp(&right.capability_id))
1138    });
1139
1140    let mapped_item_count = relationships.len();
1141    let unmapped_item_count = items
1142        .len()
1143        .checked_sub(mapped_item_count)
1144        .ok_or(ControlReferenceMappingError::CountOverflow)?;
1145
1146    Ok(ControlReferenceMappingAudit {
1147        selected: true,
1148        sources: REVIEWED_SOURCES.to_vec(),
1149        relationships,
1150        considered_item_count: items.len(),
1151        mapped_item_count,
1152        unmapped_item_count,
1153        reference_link_count,
1154        omitted_item_count: 0,
1155        omitted_reference_link_count: 0,
1156        external_activity: ControlReferenceExternalActivity {
1157            target_request_count: 0,
1158            provider_request_count: 0,
1159            source_retrieval: ControlReferenceSourceRetrievalState::NotPerformed,
1160        },
1161        claim_limits: ControlReferenceClaimLimits {
1162            control_assessment: ControlAssessmentState::NotPerformed,
1163            compliance: ControlReferenceClaimState::NotEstablished,
1164            certification: ControlReferenceClaimState::NotEstablished,
1165            legal_conclusion: ControlReferenceClaimState::NotEstablished,
1166            source_authentication: ControlReferenceClaimState::NotEstablished,
1167        },
1168    })
1169}
1170
1171fn rule_to_link(rule: &MappingRule) -> ControlReferenceLink {
1172    ControlReferenceLink {
1173        rule_id: rule.rule_id,
1174        source_id: rule.source_id,
1175        framework_id: rule.framework_id,
1176        control_reference: rule.control_reference,
1177        control_title: rule.control_title,
1178        mapping_basis: rule.mapping_basis,
1179        applicability: rule.applicability,
1180        applicability_condition: rule.applicability_condition,
1181        assurance: rule.assurance,
1182        original_mapping_rationale: rule.original_mapping_rationale,
1183    }
1184}
1185
1186fn validate_reference_links(
1187    references: &[ControlReferenceLink],
1188) -> Result<(), ControlReferenceMappingError> {
1189    if references.len() > MAX_CONTROL_REFERENCE_LINKS_PER_ITEM {
1190        return Err(ControlReferenceMappingError::TooManyReferencesPerItem);
1191    }
1192    let mut identities = BTreeSet::new();
1193    for reference in references {
1194        if !identities.insert((
1195            reference.rule_id,
1196            reference.source_id,
1197            reference.control_reference,
1198        )) {
1199            return Err(ControlReferenceMappingError::DuplicateControlReference);
1200        }
1201    }
1202    Ok(())
1203}
1204
1205#[cfg(test)]
1206mod tests {
1207    use super::*;
1208
1209    fn view<'a>(
1210        fingerprint: &'a str,
1211        capability_id: &'a str,
1212        cwe: Option<&'a str>,
1213        basis: ControlReferenceItemBasisClass,
1214    ) -> MappingItemView<'a> {
1215        MappingItemView {
1216            fingerprint,
1217            capability_id,
1218            cwe,
1219            basis,
1220        }
1221    }
1222
1223    fn positive_views() -> Vec<MappingItemView<'static>> {
1224        vec![
1225            view(
1226                "fp-03",
1227                "web.review.ssti.structural-evaluation@1",
1228                Some("CWE-1336"),
1229                ControlReferenceItemBasisClass::Differential,
1230            ),
1231            view(
1232                "fp-01",
1233                "web.review.sql.structural-differential@1",
1234                Some("CWE-89"),
1235                ControlReferenceItemBasisClass::Differential,
1236            ),
1237            view(
1238                "fp-02",
1239                "web.review.xss.structural-boundary@1",
1240                Some("CWE-79"),
1241                ControlReferenceItemBasisClass::Differential,
1242            ),
1243            view(
1244                "fp-04",
1245                "web.review.cors.credentialed-external-origin@1",
1246                Some("CWE-942"),
1247                ControlReferenceItemBasisClass::Differential,
1248            ),
1249            view(
1250                "fp-05",
1251                "web.review.redirect.candidate-specific-external@1",
1252                Some("CWE-601"),
1253                ControlReferenceItemBasisClass::Differential,
1254            ),
1255            view(
1256                "fp-06",
1257                "authorization.resource-cross-principal-equivalence@1",
1258                None,
1259                ControlReferenceItemBasisClass::Differential,
1260            ),
1261            view(
1262                "fp-07",
1263                "ssrf.oast-repeated-outbound-interaction@1",
1264                Some("CWE-918"),
1265                ControlReferenceItemBasisClass::Differential,
1266            ),
1267            view(
1268                "fp-08",
1269                "web.passive.hsts.missing@1",
1270                None,
1271                ControlReferenceItemBasisClass::Observation,
1272            ),
1273            view(
1274                "fp-09",
1275                "web.passive.hsts.max-age-zero@1",
1276                None,
1277                ControlReferenceItemBasisClass::Observation,
1278            ),
1279            view(
1280                "fp-10",
1281                "web.passive.hsts.nonconformant@1",
1282                None,
1283                ControlReferenceItemBasisClass::Observation,
1284            ),
1285            view(
1286                "fp-11",
1287                "exposure.response-private-key-material@1",
1288                None,
1289                ControlReferenceItemBasisClass::Observation,
1290            ),
1291            view(
1292                "fp-12",
1293                "exposure.response-aws-access-key-pair@1",
1294                None,
1295                ControlReferenceItemBasisClass::Observation,
1296            ),
1297            view(
1298                "fp-13",
1299                "exposure.response-stripe-live-secret@1",
1300                None,
1301                ControlReferenceItemBasisClass::Observation,
1302            ),
1303            view(
1304                "fp-14",
1305                "exposure.response-bearer-authorization@1",
1306                None,
1307                ControlReferenceItemBasisClass::Observation,
1308            ),
1309            view(
1310                "fp-15",
1311                "web.passive.csp.missing@1",
1312                None,
1313                ControlReferenceItemBasisClass::Observation,
1314            ),
1315            view(
1316                "fp-16",
1317                "web.passive.csp.nonconformant@1",
1318                None,
1319                ControlReferenceItemBasisClass::Observation,
1320            ),
1321            view(
1322                "fp-17",
1323                "web.passive.csp.unsafe-inline-declared@1",
1324                None,
1325                ControlReferenceItemBasisClass::Observation,
1326            ),
1327            view(
1328                "fp-18",
1329                "web.passive.csp.unsafe-eval-declared@1",
1330                None,
1331                ControlReferenceItemBasisClass::Observation,
1332            ),
1333            view(
1334                "fp-19",
1335                "web.passive.x-content-type-options.missing@1",
1336                None,
1337                ControlReferenceItemBasisClass::Observation,
1338            ),
1339            view(
1340                "fp-20",
1341                "web.passive.x-content-type-options.not-nosniff@1",
1342                None,
1343                ControlReferenceItemBasisClass::Observation,
1344            ),
1345        ]
1346    }
1347
1348    #[test]
1349    fn exact_positive_matrix_maps_to_literal_independent_references() {
1350        let audit = map_item_views(&positive_views()).unwrap();
1351        let actual = audit
1352            .relationships()
1353            .iter()
1354            .map(|relationship| {
1355                let reference = &relationship.references()[0];
1356                (
1357                    relationship.item_fingerprint(),
1358                    relationship.capability_id(),
1359                    relationship.cwe(),
1360                    relationship.item_basis().as_str(),
1361                    reference.source_id(),
1362                    reference.control_reference(),
1363                    reference.control_title(),
1364                )
1365            })
1366            .collect::<Vec<_>>();
1367        let expected = vec![
1368            (
1369                "fp-01",
1370                "web.review.sql.structural-differential@1",
1371                Some("CWE-89"),
1372                "differential",
1373                OWASP_SOURCE_ID,
1374                "A05:2025",
1375                Some("Injection"),
1376            ),
1377            (
1378                "fp-02",
1379                "web.review.xss.structural-boundary@1",
1380                Some("CWE-79"),
1381                "differential",
1382                OWASP_SOURCE_ID,
1383                "A05:2025",
1384                Some("Injection"),
1385            ),
1386            (
1387                "fp-03",
1388                "web.review.ssti.structural-evaluation@1",
1389                Some("CWE-1336"),
1390                "differential",
1391                OWASP_SOURCE_ID,
1392                "A05:2025",
1393                Some("Injection"),
1394            ),
1395            (
1396                "fp-04",
1397                "web.review.cors.credentialed-external-origin@1",
1398                Some("CWE-942"),
1399                "differential",
1400                OWASP_SOURCE_ID,
1401                "A01:2025",
1402                Some("Broken Access Control"),
1403            ),
1404            (
1405                "fp-05",
1406                "web.review.redirect.candidate-specific-external@1",
1407                Some("CWE-601"),
1408                "differential",
1409                OWASP_SOURCE_ID,
1410                "A01:2025",
1411                Some("Broken Access Control"),
1412            ),
1413            (
1414                "fp-06",
1415                "authorization.resource-cross-principal-equivalence@1",
1416                None,
1417                "differential",
1418                OWASP_SOURCE_ID,
1419                "A01:2025",
1420                Some("Broken Access Control"),
1421            ),
1422            (
1423                "fp-07",
1424                "ssrf.oast-repeated-outbound-interaction@1",
1425                Some("CWE-918"),
1426                "differential",
1427                OWASP_SOURCE_ID,
1428                "A01:2025",
1429                Some("Broken Access Control"),
1430            ),
1431            (
1432                "fp-08",
1433                "web.passive.hsts.missing@1",
1434                None,
1435                "observation",
1436                OWASP_SOURCE_ID,
1437                "A02:2025",
1438                Some("Security Misconfiguration"),
1439            ),
1440            (
1441                "fp-09",
1442                "web.passive.hsts.max-age-zero@1",
1443                None,
1444                "observation",
1445                OWASP_SOURCE_ID,
1446                "A02:2025",
1447                Some("Security Misconfiguration"),
1448            ),
1449            (
1450                "fp-10",
1451                "web.passive.hsts.nonconformant@1",
1452                None,
1453                "observation",
1454                OWASP_SOURCE_ID,
1455                "A02:2025",
1456                Some("Security Misconfiguration"),
1457            ),
1458            (
1459                "fp-11",
1460                "exposure.response-private-key-material@1",
1461                None,
1462                "observation",
1463                KVKK_LAW_SOURCE_ID,
1464                "6698/Madde-12",
1465                None,
1466            ),
1467            (
1468                "fp-12",
1469                "exposure.response-aws-access-key-pair@1",
1470                None,
1471                "observation",
1472                KVKK_LAW_SOURCE_ID,
1473                "6698/Madde-12",
1474                None,
1475            ),
1476            (
1477                "fp-13",
1478                "exposure.response-stripe-live-secret@1",
1479                None,
1480                "observation",
1481                KVKK_LAW_SOURCE_ID,
1482                "6698/Madde-12",
1483                None,
1484            ),
1485            (
1486                "fp-14",
1487                "exposure.response-bearer-authorization@1",
1488                None,
1489                "observation",
1490                KVKK_LAW_SOURCE_ID,
1491                "6698/Madde-12",
1492                None,
1493            ),
1494            (
1495                "fp-15",
1496                "web.passive.csp.missing@1",
1497                None,
1498                "observation",
1499                OWASP_SOURCE_ID,
1500                "A02:2025",
1501                Some("Security Misconfiguration"),
1502            ),
1503            (
1504                "fp-16",
1505                "web.passive.csp.nonconformant@1",
1506                None,
1507                "observation",
1508                OWASP_SOURCE_ID,
1509                "A02:2025",
1510                Some("Security Misconfiguration"),
1511            ),
1512            (
1513                "fp-17",
1514                "web.passive.csp.unsafe-inline-declared@1",
1515                None,
1516                "observation",
1517                OWASP_SOURCE_ID,
1518                "A02:2025",
1519                Some("Security Misconfiguration"),
1520            ),
1521            (
1522                "fp-18",
1523                "web.passive.csp.unsafe-eval-declared@1",
1524                None,
1525                "observation",
1526                OWASP_SOURCE_ID,
1527                "A02:2025",
1528                Some("Security Misconfiguration"),
1529            ),
1530            (
1531                "fp-19",
1532                "web.passive.x-content-type-options.missing@1",
1533                None,
1534                "observation",
1535                OWASP_SOURCE_ID,
1536                "A02:2025",
1537                Some("Security Misconfiguration"),
1538            ),
1539            (
1540                "fp-20",
1541                "web.passive.x-content-type-options.not-nosniff@1",
1542                None,
1543                "observation",
1544                OWASP_SOURCE_ID,
1545                "A02:2025",
1546                Some("Security Misconfiguration"),
1547            ),
1548        ];
1549        assert_eq!(actual, expected);
1550        assert_eq!(audit.considered_item_count(), 20);
1551        assert_eq!(audit.mapped_item_count(), 20);
1552        assert_eq!(audit.unmapped_item_count(), 0);
1553        assert_eq!(audit.relationship_count(), 20);
1554        assert_eq!(audit.reference_link_count(), 24);
1555        assert_eq!(audit.omitted_item_count(), 0);
1556        assert_eq!(audit.omitted_reference_link_count(), 0);
1557    }
1558
1559    #[test]
1560    fn capability_cwe_and_basis_near_misses_do_not_map() {
1561        let near_misses = vec![
1562            view(
1563                "wrong-capability",
1564                "web.review.sql.structural-differential@2",
1565                Some("CWE-89"),
1566                ControlReferenceItemBasisClass::Differential,
1567            ),
1568            view(
1569                "wrong-cwe",
1570                "web.review.sql.structural-differential@1",
1571                Some("CWE-79"),
1572                ControlReferenceItemBasisClass::Differential,
1573            ),
1574            view(
1575                "missing-cwe",
1576                "web.review.sql.structural-differential@1",
1577                None,
1578                ControlReferenceItemBasisClass::Differential,
1579            ),
1580            view(
1581                "wrong-basis",
1582                "web.review.sql.structural-differential@1",
1583                Some("CWE-89"),
1584                ControlReferenceItemBasisClass::Verifier,
1585            ),
1586            view(
1587                "extra-cwe",
1588                "web.passive.hsts.missing@1",
1589                Some("CWE-319"),
1590                ControlReferenceItemBasisClass::Observation,
1591            ),
1592            view(
1593                "secret-differential",
1594                "exposure.response-private-key-material@1",
1595                None,
1596                ControlReferenceItemBasisClass::Differential,
1597            ),
1598        ];
1599        let audit = map_item_views(&near_misses).unwrap();
1600        assert!(audit.relationships().is_empty());
1601        assert_eq!(audit.considered_item_count(), 6);
1602        assert_eq!(audit.mapped_item_count(), 0);
1603        assert_eq!(audit.unmapped_item_count(), 6);
1604    }
1605
1606    #[test]
1607    fn source_metadata_preserves_editions_rights_and_mapping_separation() {
1608        let audit = map_item_views(&[]).unwrap();
1609        let actual = audit
1610            .sources()
1611            .iter()
1612            .map(|source| {
1613                (
1614                    source.source_id(),
1615                    source.framework_id(),
1616                    source.edition(),
1617                    source.revision_or_amendment(),
1618                    source.mapping_availability().as_str(),
1619                    source.rights_basis().as_str(),
1620                    source.source_verified_at(),
1621                )
1622            })
1623            .collect::<Vec<_>>();
1624        assert_eq!(
1625            actual,
1626            vec![
1627                (
1628                    OWASP_SOURCE_ID,
1629                    "owasp-top-10",
1630                    "2025",
1631                    None,
1632                    "reviewed_identifiers_and_titles",
1633                    "identifiers_and_titles_with_attribution",
1634                    "2026-09-20"
1635                ),
1636                (
1637                    PCI_SOURCE_ID,
1638                    "pci-dss",
1639                    "4.0.1",
1640                    Some("published-2024-06-11"),
1641                    "bibliographic_only_rights_deferred",
1642                    "bibliographic_metadata_only",
1643                    "2026-09-20"
1644                ),
1645                (
1646                    ISO_SOURCE_ID,
1647                    "iso-iec-27001",
1648                    "2022",
1649                    Some("Amd-1:2024"),
1650                    "bibliographic_only_rights_deferred",
1651                    "bibliographic_metadata_only",
1652                    "2026-09-20"
1653                ),
1654                (
1655                    KVKK_LAW_SOURCE_ID,
1656                    "kvkk-law-6698",
1657                    "6698",
1658                    Some("Madde-12"),
1659                    "relevant_technical_context_only",
1660                    "official_reference_only",
1661                    "2026-09-20"
1662                ),
1663                (
1664                    KVKK_GUIDE_SOURCE_ID,
1665                    "kvkk-personal-data-security-guide",
1666                    "2025-04",
1667                    Some("KVKK-Yayinlari-No-72"),
1668                    "relevant_technical_context_only",
1669                    "official_reference_only",
1670                    "2026-09-20"
1671                ),
1672            ]
1673        );
1674        assert_eq!(audit.source_count(), 5);
1675        assert!(audit
1676            .sources()
1677            .iter()
1678            .all(|source| source.source_url().starts_with("https://")));
1679        assert!(audit
1680            .sources()
1681            .iter()
1682            .filter(|source| matches!(source.source_id(), PCI_SOURCE_ID | ISO_SOURCE_ID))
1683            .all(|source| source.mapping_availability()
1684                == ControlReferenceMappingAvailability::BibliographicOnlyRightsDeferred));
1685        assert!(audit.relationships().is_empty());
1686    }
1687
1688    #[test]
1689    fn selected_empty_is_complete_transport_free_and_claim_limited() {
1690        let audit = map_item_views(&[]).unwrap();
1691        assert_eq!(
1692            audit.schema(),
1693            "security.control-reference-mapping-audit/v1"
1694        );
1695        assert_eq!(audit.policy_id(), "termivar.control-reference-mapping/v1");
1696        assert_eq!(audit.catalogue_id(), "termivar.reviewed-control-references");
1697        assert_eq!(audit.catalogue_revision(), "2026-09-20.1");
1698        assert!(audit.selected());
1699        assert_eq!(audit.considered_item_count(), 0);
1700        assert_eq!(audit.mapped_item_count(), 0);
1701        assert_eq!(audit.unmapped_item_count(), 0);
1702        assert_eq!(audit.relationship_count(), 0);
1703        assert_eq!(audit.reference_link_count(), 0);
1704        assert_eq!(audit.external_activity().target_request_count(), 0);
1705        assert_eq!(audit.external_activity().provider_request_count(), 0);
1706        assert_eq!(
1707            audit.external_activity().source_retrieval(),
1708            ControlReferenceSourceRetrievalState::NotPerformed
1709        );
1710        assert_eq!(
1711            audit.claim_limits().control_assessment(),
1712            ControlAssessmentState::NotPerformed
1713        );
1714        assert_eq!(
1715            audit.claim_limits().compliance(),
1716            ControlReferenceClaimState::NotEstablished
1717        );
1718        assert_eq!(
1719            audit.claim_limits().certification(),
1720            ControlReferenceClaimState::NotEstablished
1721        );
1722        assert_eq!(
1723            audit.claim_limits().legal_conclusion(),
1724            ControlReferenceClaimState::NotEstablished
1725        );
1726        assert_eq!(
1727            audit.claim_limits().source_authentication(),
1728            ControlReferenceClaimState::NotEstablished
1729        );
1730    }
1731
1732    #[test]
1733    fn order_is_independent_and_relationships_are_fingerprint_sorted() {
1734        let forward = positive_views();
1735        let mut reverse = forward.clone();
1736        reverse.reverse();
1737        let left = map_item_views(&forward).unwrap();
1738        let right = map_item_views(&reverse).unwrap();
1739        assert_eq!(left, right);
1740        let fingerprints = left
1741            .relationships()
1742            .iter()
1743            .map(ControlReferenceRelationship::item_fingerprint)
1744            .collect::<Vec<_>>();
1745        assert!(fingerprints.windows(2).all(|pair| pair[0] < pair[1]));
1746    }
1747
1748    #[test]
1749    fn duplicate_input_fingerprints_are_rejected_before_mapping() {
1750        let items = vec![
1751            view(
1752                "duplicate",
1753                "web.passive.hsts.missing@1",
1754                None,
1755                ControlReferenceItemBasisClass::Observation,
1756            ),
1757            view(
1758                "duplicate",
1759                "unmapped.capability@1",
1760                None,
1761                ControlReferenceItemBasisClass::Observation,
1762            ),
1763        ];
1764        assert_eq!(
1765            map_item_views(&items),
1766            Err(ControlReferenceMappingError::DuplicateItemFingerprint)
1767        );
1768    }
1769
1770    #[test]
1771    fn item_limit_is_rejected_instead_of_truncating() {
1772        let fingerprints = (0..=MAX_CONTROL_REFERENCE_MAPPING_ITEMS)
1773            .map(|index| format!("fp-{index:04}"))
1774            .collect::<Vec<_>>();
1775        let items = fingerprints
1776            .iter()
1777            .map(|fingerprint| {
1778                view(
1779                    fingerprint,
1780                    "unmapped.capability@1",
1781                    None,
1782                    ControlReferenceItemBasisClass::Observation,
1783                )
1784            })
1785            .collect::<Vec<_>>();
1786        assert_eq!(
1787            map_item_views(&items),
1788            Err(ControlReferenceMappingError::TooManyItems)
1789        );
1790    }
1791
1792    #[test]
1793    fn duplicate_reference_links_and_per_item_limit_are_rejected() {
1794        let link = rule_to_link(&RULES[0]);
1795        assert_eq!(
1796            validate_reference_links(&[link.clone(), link]),
1797            Err(ControlReferenceMappingError::DuplicateControlReference)
1798        );
1799        let too_many = (0..=MAX_CONTROL_REFERENCE_LINKS_PER_ITEM)
1800            .map(|_| rule_to_link(&RULES[0]))
1801            .collect::<Vec<_>>();
1802        assert_eq!(
1803            validate_reference_links(&too_many),
1804            Err(ControlReferenceMappingError::TooManyReferencesPerItem)
1805        );
1806    }
1807
1808    #[test]
1809    fn kvkk_links_are_context_only_and_owasp_links_retain_exact_edition() {
1810        let audit = map_item_views(&positive_views()).unwrap();
1811        let secret = audit
1812            .relationships()
1813            .iter()
1814            .find(|row| row.item_fingerprint() == "fp-11")
1815            .unwrap();
1816        assert_eq!(secret.references().len(), 2);
1817        let link = &secret.references()[0];
1818        assert_eq!(
1819            link.applicability(),
1820            ControlReferenceApplicability::ApplicabilityUnestablished
1821        );
1822        assert_eq!(
1823            link.assurance(),
1824            ControlReferenceMappingAssurance::RelevantTechnicalContext
1825        );
1826        assert_eq!(link.control_title(), None);
1827        assert!(link
1828            .original_mapping_rationale()
1829            .contains("legal applicability were not established"));
1830        let guide = &secret.references()[1];
1831        assert_eq!(guide.source_id(), KVKK_GUIDE_SOURCE_ID);
1832        assert_eq!(guide.framework_id(), "kvkk-personal-data-security-guide");
1833        assert_eq!(guide.control_reference(), "KVKK-Rehber-72/2025-04");
1834        assert_eq!(guide.control_title(), None);
1835        assert_eq!(
1836            guide.applicability(),
1837            ControlReferenceApplicability::ApplicabilityUnestablished
1838        );
1839        assert_eq!(
1840            guide.assurance(),
1841            ControlReferenceMappingAssurance::RelevantTechnicalContext
1842        );
1843        assert!(guide
1844            .original_mapping_rationale()
1845            .contains("does not establish personal-data scope"));
1846
1847        let sql = audit
1848            .relationships()
1849            .iter()
1850            .find(|row| row.item_fingerprint() == "fp-01")
1851            .unwrap();
1852        assert_eq!(sql.references()[0].control_reference(), "A05:2025");
1853        assert_ne!(sql.references()[0].control_reference(), "A03:2021");
1854    }
1855}