1use std::collections::BTreeSet;
9
10use thiserror::Error;
11
12use crate::web_runtime::{AssessmentBasis, AssessmentItem};
13
14pub const CONTROL_REFERENCE_MAPPING_AUDIT_SCHEMA: &str =
16 "security.control-reference-mapping-audit/v1";
17pub const CONTROL_REFERENCE_MAPPING_POLICY_ID: &str = "termivar.control-reference-mapping/v1";
19pub const CONTROL_REFERENCE_MAPPING_CATALOGUE_ID: &str = "termivar.reviewed-control-references";
21pub const CONTROL_REFERENCE_MAPPING_CATALOGUE_REVISION: &str = "2026-09-20.1";
23pub const CONTROL_REFERENCE_SOURCE_VERIFIED_AT: &str = "2026-09-20";
25
26pub const MAX_CONTROL_REFERENCE_MAPPING_ITEMS: usize = 4_096;
28pub const MAX_CONTROL_REFERENCE_MAPPING_RELATIONSHIPS: usize = 4_096;
30pub const MAX_CONTROL_REFERENCE_MAPPING_LINKS: usize = 16_384;
32pub 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
49pub enum ControlReferenceAuthorityKind {
50 CommunitySecurityProject,
52 IndustryStandardsBody,
54 InternationalStandardsOrganization,
56 StatutoryAuthority,
58}
59
60impl ControlReferenceAuthorityKind {
61 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
74pub enum ControlReferenceMappingAvailability {
75 ReviewedIdentifiersAndTitles,
77 BibliographicOnlyRightsDeferred,
79 RelevantTechnicalContextOnly,
81}
82
83impl ControlReferenceMappingAvailability {
84 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
96pub enum ControlReferenceRightsBasis {
97 IdentifiersAndTitlesWithAttribution,
99 BibliographicMetadataOnly,
101 OfficialReferenceOnly,
103}
104
105impl ControlReferenceRightsBasis {
106 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
118pub enum ControlReferenceItemBasisClass {
119 Observation,
121 Differential,
123 Verifier,
125}
126
127impl ControlReferenceItemBasisClass {
128 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
148pub enum ControlReferenceMappingBasis {
149 ExactCapabilityCweAndAssessmentBasis,
151 ExactCapabilityAndAssessmentBasis,
153}
154
155impl ControlReferenceMappingBasis {
156 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
169pub enum ControlReferenceApplicability {
170 ConditionalTechnicalContext,
172 ApplicabilityUnestablished,
174}
175
176impl ControlReferenceApplicability {
177 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
188pub enum ControlReferenceMappingAssurance {
189 ReviewedPrimarySourceIdentifier,
191 RelevantTechnicalContext,
193}
194
195impl ControlReferenceMappingAssurance {
196 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
207pub enum ControlReferenceSourceRetrievalState {
208 NotPerformed,
210}
211
212impl ControlReferenceSourceRetrievalState {
213 pub const fn as_str(self) -> &'static str {
215 match self {
216 Self::NotPerformed => "not_performed",
217 }
218 }
219}
220
221#[derive(Clone, Copy, Debug, Eq, PartialEq)]
223pub enum ControlAssessmentState {
224 NotPerformed,
226}
227
228impl ControlAssessmentState {
229 pub const fn as_str(self) -> &'static str {
231 match self {
232 Self::NotPerformed => "not_performed",
233 }
234 }
235}
236
237#[derive(Clone, Copy, Debug, Eq, PartialEq)]
239pub enum ControlReferenceClaimState {
240 NotEstablished,
242}
243
244impl ControlReferenceClaimState {
245 pub const fn as_str(self) -> &'static str {
247 match self {
248 Self::NotEstablished => "not_established",
249 }
250 }
251}
252
253#[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 pub const fn source_id(&self) -> &'static str {
270 self.source_id
271 }
272
273 pub const fn framework_id(&self) -> &'static str {
275 self.framework_id
276 }
277
278 pub const fn edition(&self) -> &'static str {
280 self.edition
281 }
282
283 pub const fn revision_or_amendment(&self) -> Option<&'static str> {
285 self.revision_or_amendment
286 }
287
288 pub const fn authority_kind(&self) -> ControlReferenceAuthorityKind {
290 self.authority_kind
291 }
292
293 pub const fn source_url(&self) -> &'static str {
295 self.source_url
296 }
297
298 pub const fn source_verified_at(&self) -> &'static str {
300 self.source_verified_at
301 }
302
303 pub const fn mapping_availability(&self) -> ControlReferenceMappingAvailability {
305 self.mapping_availability
306 }
307
308 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#[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 pub const fn rule_id(&self) -> &'static str {
394 self.rule_id
395 }
396
397 pub const fn source_id(&self) -> &'static str {
399 self.source_id
400 }
401
402 pub const fn framework_id(&self) -> &'static str {
404 self.framework_id
405 }
406
407 pub const fn control_reference(&self) -> &'static str {
409 self.control_reference
410 }
411
412 pub const fn control_title(&self) -> Option<&'static str> {
414 self.control_title
415 }
416
417 pub const fn mapping_basis(&self) -> ControlReferenceMappingBasis {
419 self.mapping_basis
420 }
421
422 pub const fn applicability(&self) -> ControlReferenceApplicability {
424 self.applicability
425 }
426
427 pub const fn applicability_condition(&self) -> &'static str {
429 self.applicability_condition
430 }
431
432 pub const fn assurance(&self) -> ControlReferenceMappingAssurance {
434 self.assurance
435 }
436
437 pub const fn original_mapping_rationale(&self) -> &'static str {
439 self.original_mapping_rationale
440 }
441}
442
443#[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 pub fn item_fingerprint(&self) -> &str {
456 &self.item_fingerprint
457 }
458
459 pub fn capability_id(&self) -> &str {
461 &self.capability_id
462 }
463
464 pub fn cwe(&self) -> Option<&str> {
466 self.cwe.as_deref()
467 }
468
469 pub const fn item_basis(&self) -> ControlReferenceItemBasisClass {
471 self.item_basis
472 }
473
474 pub fn references(&self) -> &[ControlReferenceLink] {
476 &self.references
477 }
478}
479
480#[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 pub const fn target_request_count(&self) -> u64 {
491 self.target_request_count
492 }
493
494 pub const fn provider_request_count(&self) -> u64 {
496 self.provider_request_count
497 }
498
499 pub const fn source_retrieval(&self) -> ControlReferenceSourceRetrievalState {
501 self.source_retrieval
502 }
503}
504
505#[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 pub const fn control_assessment(&self) -> ControlAssessmentState {
518 self.control_assessment
519 }
520
521 pub const fn compliance(&self) -> ControlReferenceClaimState {
523 self.compliance
524 }
525
526 pub const fn certification(&self) -> ControlReferenceClaimState {
528 self.certification
529 }
530
531 pub const fn legal_conclusion(&self) -> ControlReferenceClaimState {
533 self.legal_conclusion
534 }
535
536 pub const fn source_authentication(&self) -> ControlReferenceClaimState {
538 self.source_authentication
539 }
540}
541
542#[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 pub const fn schema(&self) -> &'static str {
561 CONTROL_REFERENCE_MAPPING_AUDIT_SCHEMA
562 }
563
564 pub const fn policy_id(&self) -> &'static str {
566 CONTROL_REFERENCE_MAPPING_POLICY_ID
567 }
568
569 pub const fn catalogue_id(&self) -> &'static str {
571 CONTROL_REFERENCE_MAPPING_CATALOGUE_ID
572 }
573
574 pub const fn catalogue_revision(&self) -> &'static str {
576 CONTROL_REFERENCE_MAPPING_CATALOGUE_REVISION
577 }
578
579 pub const fn selected(&self) -> bool {
581 self.selected
582 }
583
584 pub fn sources(&self) -> &[ControlReferenceSource] {
586 &self.sources
587 }
588
589 pub fn relationships(&self) -> &[ControlReferenceRelationship] {
591 &self.relationships
592 }
593
594 pub const fn considered_item_count(&self) -> usize {
596 self.considered_item_count
597 }
598
599 pub const fn mapped_item_count(&self) -> usize {
601 self.mapped_item_count
602 }
603
604 pub const fn unmapped_item_count(&self) -> usize {
606 self.unmapped_item_count
607 }
608
609 pub fn relationship_count(&self) -> usize {
611 self.relationships.len()
612 }
613
614 pub const fn reference_link_count(&self) -> usize {
616 self.reference_link_count
617 }
618
619 pub const fn omitted_item_count(&self) -> usize {
621 self.omitted_item_count
622 }
623
624 pub const fn omitted_reference_link_count(&self) -> usize {
626 self.omitted_reference_link_count
627 }
628
629 pub fn source_count(&self) -> usize {
631 self.sources.len()
632 }
633
634 pub const fn external_activity(&self) -> ControlReferenceExternalActivity {
636 self.external_activity
637 }
638
639 pub const fn claim_limits(&self) -> ControlReferenceClaimLimits {
641 self.claim_limits
642 }
643}
644
645#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
647pub enum ControlReferenceMappingError {
648 #[error("control-reference mapping item limit exceeded")]
650 TooManyItems,
651 #[error("duplicate assessment-item fingerprint")]
653 DuplicateItemFingerprint,
654 #[error("control-reference mapping relationship limit exceeded")]
656 TooManyRelationships,
657 #[error("per-item control-reference limit exceeded")]
659 TooManyReferencesPerItem,
660 #[error("control-reference mapping link limit exceeded")]
662 TooManyReferenceLinks,
663 #[error("duplicate control-reference link")]
665 DuplicateControlReference,
666 #[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
1070pub 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}