feat: consolidate /expenses into /supplier-invoices, dual-channel support feedback, compliance skills (#417)

* feat(supplier-invoices): consolidate /expenses into /supplier-invoices and add bank matching

Collapse the duplicate AP entry points by redirecting /expenses, /expenses/new,
and /expenses/[id] into the canonical /supplier-invoices routes, and absorb the
expense-entry flow into /supplier-invoices/new. Add a BankTransactionPicker so
a supplier invoice can be registered and matched to an existing outgoing bank
transaction in one step. Extend the transaction → invoice match dialog to
handle supplier invoices alongside customer invoices, including the new
/match-supplier-invoice endpoint and Swedish copy variants. Update the sidebar
to point Leverantörsfakturor at /supplier-invoices and hide the legacy entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(support): send feedback to both Recapt and email channels

Previously submitFeedback used Recapt when present and only fell back to email
on failure, so feedback captured by the SDK never reached the support inbox.
Always POST to /api/support/contact in parallel with the Recapt call and
return the list of channels that succeeded; feedback is considered delivered
if either channel succeeds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(skills): add compliance skill references for GDPR, ISO 27001, OSS, OWASP ASVS, SOC 2

Add reference material for five compliance domains alongside the existing
.claude/skills/ set so future audits and CI gating work has a documented
mapping to controls, violation patterns, tool orchestration, and cross-framework
crosswalks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(supplier-invoices): address review issues in register-and-match flow

- BankTransactionPicker: move createClient() inside the effect and drop the
  supabase client from the deps array. Calling createClient() in the component
  body produced a fresh reference on every render, which combined with the
  setIsLoading(true) inside the effect to fire an infinite re-fetch loop while
  the dialog was open.
- /supplier-invoices/new: replace the submitMode useState with a useRef.
  setSubmitMode() in the button onClick and the read in the form onSubmit run
  in the same React event batch, so onSubmit always saw the previous render's
  value and the bank picker never opened on first use.
- /supplier-invoices/new: route AB companies through the existing review
  dialog before booking the register-and-match flow. handlePickTransaction now
  stores the picked transaction and opens the review dialog for AB; on
  confirm, handleConfirm posts the create and then matches the stored
  transaction. EF retains the one-step create+approve+match path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(transactions): populate potential_supplier_invoice so the match flow is reachable

fetchTransactions and loadMoreTransactions previously joined potential_invoice
from the invoices table but never looked up potential_supplier_invoice, so
TransactionInboxCard's hasSupplierInvoiceMatch check was always false and
handleConfirmInvoiceMatch's supplier branch was unreachable. Mirror the
existing customer-invoice pipeline: collect potential_supplier_invoice_id
values, batch-fetch the matching supplier_invoices (with their supplier),
build a map, and spread the result onto each TransactionWithInvoice in
parallel with the customer-invoice fetch.

Update uncategorizedTransactions sort and transactionsWithMatches filter to
also recognize supplier-invoice candidates so they bubble to the top of the
inbox and aren't excluded from match-driven views.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-05-07 18:13:43 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 8a1c7f0714
commit 15d4f429f3
52 changed files with 8103 additions and 2043 deletions
@@ -0,0 +1,93 @@
---
name: gdpr-cicd-compliance
description: GDPR (Regulation (EU) 2016/679) reference for repository and CI/CD compliance automation. Covers the primary articles (Art. 5 principles, Art. 6/9 lawful bases, Art. 15-22 data subject rights, Art. 25 privacy by design, Art. 30 RoPA, Art. 32 security, Art. 33/34 breach notification, Art. 35 DPIA, Art. 44-49 international transfers), EDPB Guidelines 9/2022 and Recommendations 01/2020, the .compliance/ canonical document set (RoPA, DPIA, DSAR runbook, TIA, incident response), Schrems II transfer mechanics, the Fideslang privacy taxonomy, cross-framework mappings (NIST 800-53, ISO 27001:2022, SOC 2 TSC, CIS v8) via the Secure Controls Framework STRM, three-tier verification taxonomy (deterministic / agentic / out-of-repo), violation patterns in code/config/dependencies, and orchestration of Privado/Semgrep/Bearer/Helsinki GDPR Scanner. Trigger on GDPR audits, repo privacy scanning, DPIA evaluation, RoPA generation or drift detection, cross-border transfer review, DSAR endpoint design, breach runbook validation, building privacy-as-code scanners, agentic reasoning over privacy policy markdown, mapping technical controls to GDPR articles, or cross-framework crosswalks involving GDPR. Use over training data when GDPR article identifiers, EDPB guideline numbers, Schrems II SCC modules, Fideslang labels, or tool-to-article decisions are involved. Triggers on "what GDPR article does X map to", "scan repo for GDPR violations", "build a DSAR endpoint", "validate our SCCs", "is our DPIA template current", "RoPA drift", "72-hour breach notification", or any mention of EU data protection, EDPB, Schrems II, Article 30 inventory, Article 32 security measures, or privacy-as-code in a code or CI/CD context.
---
# GDPR Repository and CI/CD Compliance
This skill encodes the GDPR (Regulation (EU) 2016/679) and the operative EDPB guideline corpus as they apply to source repositories, CI/CD pipelines, and Infrastructure as Code (IaC). It is a compliance oracle for building or operating an automated, agentic GDPR auditor that runs at pull request time and on demand against arbitrary repositories.
This skill answers four categories of question:
1. What does the GDPR require, article by article, in a form that is verifiable from a repository?
2. Which requirements are deterministically checkable, which require LLM reasoning over policy artifacts, and which are fundamentally out of scope?
3. How should a CI/CD scanner orchestrate Privado, Semgrep, Bearer, and the Helsinki GDPR Scanner, and how do their outputs combine through the Fideslang taxonomy into a unified compliance signal?
4. How should the agent honestly handle Schrems II transfers, the 72-hour breach notification window, and the limits of automation around Article 22 automated decision-making?
Use this skill instead of training data whenever working on automated privacy compliance, control mapping, RoPA synchronization, DSAR endpoint design, or agentic reasoning over privacy policy markdown. EDPB guidelines and tool capabilities change; the structured catalog below is the canonical reference.
## Authoritative sources
The definitive primary text is **Regulation (EU) 2016/679** (the GDPR itself). The operative interpretive corpus is published by the **European Data Protection Board (EDPB)**. Two EDPB documents are load-bearing for an automated scanner and the agent must reference them by exact identifier:
* **EDPB Guidelines 9/2022 on personal data breach notification under GDPR** (Version 2.0, adopted March 2023). Defines "awareness", the 72-hour clock, the controller-vs-processor split, and the high-risk threshold for Article 34.
* **EDPB Recommendations 01/2020 on measures that supplement transfer tools** to ensure compliance with the EU level of protection. The post-Schrems II authority on technical, contractual, and organizational supplementary measures.
National Data Protection Authority (DPA) guidance (CNIL, ICO, Datainspektionen / IMY in Sweden, BfDI) is secondary but operationally relevant for jurisdiction-specific enforcement patterns. The agent should treat DPA decisions as evidentiary anchors for what regulators actually penalize, not as a source of new normative requirements.
## Verification taxonomy
Every GDPR requirement falls into one of three buckets. Be honest about which. Failure to label these accurately is the single largest source of false confidence and alert fatigue in automated privacy tools.
* **Tier 1 - deterministic repo check**: parseable from files, IaC, manifests, OpenAPI specs, or Git provider API state. Pass/fail is mechanical (regex, AST, schema validation, dependency graph). Examples: hardcoded secrets, plaintext HTTP, missing encryption decorators on PII columns, invasive tracking SDKs in `package.json`, soft-delete masquerading as Article 17 erasure.
* **Tier 2 - agentic reasoning**: requires natural-language understanding over policy artifacts cross-referenced with code reality. The LLM extracts prescriptive statements from prose and validates technical implementation against them. Examples: RoPA-vs-actual-data-flow drift, purpose limitation across an OpenAPI endpoint and its controller, consent logic vs. cookie policy claims.
* **Tier 3 - out of repo**: cannot be verified from repository contents at all. The scanner can at most confirm a policy artifact mandating the control exists, plus a verifiable evidence pointer (URI, signed credential, contract management API link). Examples: physical security, employee privacy training completion, executed DPA/SCC contracts, regulator notification logs.
See `references/verification-tiers.md` for the per-article tier assignment and the rationale.
## Catalog of areas
Detailed content lives in references. Load the relevant file when working on that area; do not load all references preemptively.
* **GDPR articles (operative subset)**: see `references/gdpr-articles.md`. Article-by-article requirements, technical intent, and the verification tier each maps to. Covers Art. 5 (principles), Art. 6 (lawful basis), Art. 9 (special categories), Art. 15-22 (data subject rights), Art. 25 (privacy by design and by default), Art. 30 (RoPA), Art. 32 (security), Art. 33-34 (breach notification), Art. 35 (DPIA), Art. 44-49 (international transfers).
* **Canonical document set**: see `references/canonical-documents.md`. The expected `.compliance/` layout, file formats, structural validation rules, and the agentic checks that compare each artifact against codebase reality.
* **Cross-framework mapping**: see `references/cross-framework-mapping.md`. The Secure Controls Framework STRM-based crosswalk to NIST 800-53 Rev 5, ISO/IEC 27001:2022 Annex A, SOC 2 TSC, and CIS Controls v8. Use this when emitting multi-framework evidence tags from a single technical check.
* **Violation patterns**: see `references/violation-patterns.md`. Code-level, configuration-level, and dependency-level anti-patterns drawn from regulatory fines and architectural failure modes (CNIL Apple/Voodoo Games 2023, Clearview AI biometric scraping, SalesLoft/Drift OAuth supply chain, Shai-Hulud npm backdoor, Enel customer-management failure).
* **Tool orchestration**: see `references/tool-orchestration.md`. How to wrap Privado, Semgrep, Bearer, and the Helsinki GDPR Scanner; how to standardize their outputs through the Fideslang privacy taxonomy (data categories, data uses, data subjects); how to detect Article 9 violations from Fideslang label combinations.
* **DSAR API patterns**: see `references/dsar-api-patterns.md`. RESTful patterns for Articles 15, 17, and 20 endpoints; JSON Schema validation enforcing purpose limitation; OAuth scoping; redaction of third-party PII under Article 15(4).
* **Schrems II and international transfers**: see `references/transfers-schrems-ii.md`. Geographic data sink mapping, 2021 modular SCC verification, Transfer Impact Assessment automation, and how to deterministically prove supplementary technical measures (CMK enforcement, EU-resident key custody) in IaC.
* **Breach notification runbook**: see `references/breach-notification.md`. The 72-hour mandate decomposed into observability, runbook structure, DPO escalation, and Article 34 high-risk communication templates per EDPB Guidelines 9/2022.
* **Agentic prompt templates**: see `references/agentic-prompts.md`. Pre-built LLM prompts for the Tier 2 checks (RoPA drift, purpose limitation, consent logic), tuned to match the observed accuracy ceilings from the GDPR-Bench-Android study (Qwen2.5-72B 61.6% line-level Accuracy@1, ReAct agent 17.4% file-level Accuracy@1).
* **Limits of automation**: see `references/limitations.md`. Article 22 automated decision-making, Article 9 substantial public interest, the meaningful-human-involvement threshold, and the boundary at which the scanner must hand off to a Data Protection Officer.
## Canonical document set (privacy as code)
A well-architected, GDPR-aware repository contains a `.compliance/` (or `.privacy/`) directory holding machine-readable versions of the privacy artifacts. Treating privacy documentation as code ensures policies evolve synchronously with the software architecture; without this, the repository drifts into latent non-compliance the moment data flows change.
The agent expects the following at minimum:
* `.compliance/ropa.json` or `.compliance/ropa.yaml` - Record of Processing Activities (Art. 30). Compared against the dynamically generated data flow graph from Privado.
* `.compliance/dpia_inventory/` - Data Protection Impact Assessments (Art. 35), one per high-risk processing operation. Markdown or JSON, structured against the EDPB DPIA template.
* `.compliance/dsar_runbook.md` - Technical playbooks for Articles 15, 16, 17, and 20 across the microservice architecture.
* `.compliance/transfers/` - Transfer Impact Assessments and references to executed 2021 modular SCCs, per Schrems II.
* `.compliance/incident_response.md` - The 72-hour notification runbook, with hardcoded DPO and legal escalation paths.
* `.compliance/privacy_policy.md` - The forward-facing notice. The agent verifies semantic alignment between the claims here and the actual data categories processed by source code.
If any of these are missing, structurally invalid, or semantically misaligned with the codebase, the scanner raises a compliance violation and blocks the pipeline. Detailed validation rules per artifact live in `references/canonical-documents.md`.
## Trigger and suppression heuristics
Executing a full agentic LLM audit on every commit is computationally prohibitive and erodes developer trust. The skill applies dynamic depth based on diff content.
**Full agentic audit triggers when the PR modifies:**
* Database schema migrations
* OpenAPI specifications or GraphQL schemas
* Dependency manifests (`package.json`, `pom.xml`, `requirements.txt`, `Cargo.toml`, `go.mod`)
* Authentication, authorization, or session controllers
* Files within `.compliance/`
* IaC templates that provision storage, network egress, or KMS
* Any file containing Fideslang annotations
**Deterministic-only (Tier 1) scan suffices for:**
* Frontend CSS-only changes
* Markdown typo fixes outside `.compliance/`
* Test fixture updates with synthetic data
* Documentation in `docs/` not referenced by `.compliance/`
**Suppression**: developers may suppress a known false positive with an inline comment carrying a justification (`// gdpr-suppress: <rule-id> - <reason>`). All suppressions are logged and surfaced in a quarterly human review batch. Unjustified or expired suppressions revoke automatically.
## Honest limits
The scanner cannot adjudicate Article 22 (automated decision-making with legal or similarly significant effects), Article 9(2)(g) substantial public interest, or the proportionality balancing inherent to risk-based provisions. It can detect that an ML model is deployed; it cannot evaluate whether the human reviewer in the loop is meaningful or a rubber stamp. When such patterns are detected, the scanner halts the pipeline and flags for mandatory DPO review rather than rendering a pass/fail. See `references/limitations.md` for the full enumeration and the prompt patterns the agent uses to surface these to a human.
The goal is to eliminate routine, deterministic errors at the baseline and to escalate genuine ambiguity, not to replace the DPO. A scanner that pretends to do the latter is itself a compliance risk.
@@ -0,0 +1,225 @@
# Agentic Prompt Templates
The Tier 2 checks require an LLM to reason over policy markdown and code together. The prompt templates below are the operational core of the agentic auditor. Each is structured around three components: the policy artifact, the code-derived ground truth, and the specific question the agent is asked to answer.
The GDPR-Bench-Android study established realistic accuracy ceilings for this class of work. Qwen2.5-72B reached 61.6% Accuracy@1 at the line level; a ReAct-style agent reached 17.4% Accuracy@1 at the file level. The takeaway: prompt narrowly, give the agent both halves of the comparison explicitly, and prefer many small, focused checks over one omnibus prompt.
## Prompt: RoPA drift detection
The most important Tier 2 check. The agent reconciles `ropa.yaml` against the Privado-derived data flow graph.
```
You are a GDPR Article 30 compliance auditor. You are given two artifacts:
1. The repository's declared Record of Processing Activities (ropa.yaml).
2. The actual data flows detected in the source code by Privado (privado.json).
Your task: enumerate flows that are present in one artifact but not the other.
For each discrepancy, output:
- Direction: "in_code_not_in_ropa" or "in_ropa_not_in_code"
- Source: file:line if from code, or RoPA entry id if from RoPA
- Sink: the destination service or processing operation
- Fideslang data category
- Severity: "critical" if the flow involves user.health.*, user.biometric.*, user.genetic.*, user.financial.*, or user.behavior.children.*; "high" otherwise
Output format: JSON array. No prose.
A flow is "the same" if it has the same Fideslang data_category and the same sink. Different field names with the same data_category are the same flow.
ROPA:
<paste ropa.yaml>
PRIVADO OUTPUT:
<paste privado.json subset relevant to flows>
```
The output of this prompt feeds directly into the finding emitter; no further LLM judgment is required.
## Prompt: purpose limitation check
For a single API endpoint, validate that the actual data returned is commensurate with the documented business purpose.
```
You are a GDPR Article 5(1)(b) auditor. You are given:
1. The OpenAPI specification for a single endpoint, including its summary, description, and tags.
2. The controller method body for the endpoint.
3. The Fideslang annotations on the response schema fields.
Your task: assess whether the data returned exceeds what the documented purpose requires.
For each field returned that you assess as exceeding the purpose, output:
- Field name
- Fideslang data_category
- Why this field is not necessary for the documented purpose (one sentence)
- Suggested remediation (drop the field, gate it behind a different scope, or expand the documented purpose)
If all fields are commensurate with the purpose, output an empty array.
OPENAPI:
<paste relevant endpoint spec>
CONTROLLER:
<paste controller method>
Output format: JSON array. No prose.
```
This check is sensitive to false positives. The agent's threshold for flagging should be high - a finding should be defensible to the implementing engineer, not pedantic. Pair the prompt with a developer suppression mechanism that requires a justification.
## Prompt: consent logic vs. cookie policy
```
You are a GDPR Article 6(1)(a) and Article 7 auditor. You are given:
1. The repository's consent_mappings.yaml documenting which Fideslang data_uses require consent and which cookies map to which categories.
2. The frontend consent management code (the part that decides which scripts and cookies are activated).
3. The list of scripts and cookies that fire on the marketing landing page.
Your task: identify any script or cookie tagged as a consent-required category that fires before the user has given consent.
For each violation, output:
- Script or cookie name
- Required consent category (per consent_mappings.yaml)
- Why the current code activates it without consent (one sentence)
- File:line reference if available
Output format: JSON array. No prose.
If you cannot determine the answer with high confidence from the inputs, output a single object with field "uncertainty" describing what you would need to determine the answer.
CONSENT MAPPINGS:
<paste consent_mappings.yaml>
FRONTEND CONSENT CODE:
<paste relevant code>
PAGE LOAD MANIFEST:
<paste from helsinki-gdpr-scanner output>
```
The "uncertainty" output channel matters. This check has high false-positive potential because consent management code is genuinely complex. The agent should be trained to escape rather than guess.
## Prompt: DPIA-vs-implementation drift
```
You are a GDPR Article 35 auditor. You are given:
1. A DPIA document for a high-risk processing operation.
2. The relevant section(s) of the codebase that implement the operation.
Your task: extract every technical or organizational measure named in the DPIA's "measures envisaged to address the risks" section, and for each, judge whether the codebase implements it.
For each measure, output:
- Measure (verbatim from the DPIA)
- Implementation status: "implemented" | "partially_implemented" | "not_implemented" | "cannot_assess_from_repository"
- Evidence: file:line references for "implemented" and "partially_implemented"; brief explanation of what is missing for "not_implemented"; brief explanation of what is needed to assess for "cannot_assess_from_repository"
Output format: JSON array. No prose.
DPIA:
<paste DPIA markdown>
RELEVANT CODE:
<paste code; if too large, paste an index of files and let the user iterate>
```
The "cannot_assess_from_repository" output is critical. Some DPIA measures are organizational (training, governance, board oversight) and are out of repo by nature. The agent must label these honestly rather than judging them as not-implemented.
## Prompt: privacy policy alignment
```
You are a GDPR Article 13 auditor. You are given:
1. The repository's privacy_policy.md.
2. The union of Fideslang data_categories actually processed in the codebase.
3. The list of recipients (third-party services, processors) actually invoked from the codebase.
4. The retention periods enforced by retention scripts in the codebase.
Your task: identify discrepancies where the privacy policy makes claims that are not borne out by the codebase, or where the codebase processes data that the privacy policy does not disclose.
For each discrepancy, output:
- Type: "policy_claims_what_code_does_not_do" | "code_does_what_policy_does_not_disclose"
- Subject: the specific data category, recipient, or retention claim
- Evidence in policy: a short excerpt
- Evidence in code: file:line if available
Output format: JSON array. No prose.
PRIVACY POLICY:
<paste privacy_policy.md>
CODE-DERIVED FACTS:
<paste structured summary>
```
This check often surfaces the most embarrassing findings. Privacy policies are written by legal teams on a quarterly cadence; code changes daily. The drift between them is, in the median repository, substantial.
## Prompt: incident response runbook completeness
```
You are a GDPR Article 33 and Article 34 auditor. You are given:
1. The repository's incident_response.md runbook.
2. EDPB Guidelines 9/2022 (Version 2.0, March 2023) phase structure as a reference.
Your task: verify the runbook addresses each of the six phases (detection and triage, assessment and containment, DPO and legal escalation, supervisory authority notification, data subject communication, post-incident documentation) and that the content for each phase is operationally specific (not aspirational).
For each phase, output:
- Phase name
- Status: "addressed" | "addressed_but_vague" | "missing"
- Evidence: a short excerpt for "addressed" and "addressed_but_vague"; for "missing", confirm absence
- For "addressed_but_vague": what specific element is needed to make it operational
Output format: JSON object keyed by phase name. No prose.
RUNBOOK:
<paste incident_response.md>
```
The "addressed_but_vague" verdict is what the agent earns its keep on. A runbook that says "the team will notify the authority within 72 hours" is technically present but operationally useless. The agent should distinguish.
## Prompt: dependency consent gating
```
You are a GDPR auditor specializing in third-party SDK consent compliance. You are given:
1. The list of dependencies in the project (package.json or equivalent).
2. The denylist of SDKs known to require explicit consent.
3. The consent management code (if any).
Your task: for each dependency on the denylist that appears in the project, determine whether its initialization is gated behind explicit consent for the appropriate Fideslang data_use.
For each dependency, output:
- Package name and version
- Reason it is on the denylist (e.g., "advertising identifier collection", "device fingerprinting")
- Consent gating status: "gated" | "not_gated" | "unable_to_determine"
- File:line of initialization
- Required consent category
Output format: JSON array. No prose.
DEPENDENCIES:
<paste manifest>
DENYLIST:
<paste denylist>
CONSENT CODE:
<paste relevant code, or note "no consent management detected">
```
A dependency on the denylist with `not_gated` status is a likely Article 6 violation in any consumer context. `unable_to_determine` is escalated to a human review; the cost of false positives in this category is reputational, so the agent should err toward escalation.
## Prompt construction principles
A few patterns repeat across these prompts and should be replicated when adding new ones:
* **Show both halves**: the agent gets the policy artifact and the code-derived facts in the same prompt. Without one, the agent guesses.
* **Constrain output to JSON**: the findings flow into a downstream emitter. Prose responses cannot be parsed reliably.
* **Provide an "uncertainty" or "cannot_assess" escape**: the agent must be able to say "I don't know" rather than guess. The downstream system treats these as escalation triggers.
* **Anchor severity in objective criteria**: "critical" if special category data, "high" otherwise. The agent's tone-driven severity guesses are not useful; explicit rules are.
* **Reference the EDPB document by exact identifier when relevant**: this anchors the LLM in the correct interpretive corpus and reduces drift toward generic privacy advice.
When iterating on these prompts based on observed false positive / negative rates, change one variable at a time and re-evaluate against a held-out set. The GDPR-Bench-Android study's accuracy numbers are a useful sanity check ceiling: if a custom prompt reports 95% accuracy on a real-world repository, it is overfitting or measuring something narrower than it claims.
@@ -0,0 +1,129 @@
# Breach Notification (Articles 33 and 34)
Article 33 imposes a 72-hour notification window from the moment the controller becomes "aware" of a personal data breach. Article 34 requires direct communication to data subjects when the breach is likely to result in a "high risk" to their rights and freedoms. EDPB Guidelines 9/2022 (Version 2.0, March 2023) define awareness, the high-risk threshold, and the controller-vs-processor split.
Compliance with the 72-hour mandate is impossible without specific technical and organizational preparedness in advance. The scanner verifies that the preparedness exists in the repository.
## "Awareness" - the operative trigger
Per EDPB Guidelines 9/2022, awareness occurs when the controller has a reasonable degree of certainty that a security incident has occurred leading to the compromise of personal data. Awareness is not when the security team first sees an alert; it is when the technical and forensic facts establish, at a reasonable level of confidence, that personal data was affected.
The 72-hour clock starts at awareness. A scanner that focuses only on the runbook misses the upstream technical question: does the observability stack actually let the team reach awareness in a timely manner?
## Tier 1 checks: observability infrastructure
The scanner parses monitoring and alerting configurations to verify that the necessary detection capacity exists.
### Required signals
The agent expects detection rules for at minimum:
* **Mass exfiltration patterns**: outbound network volume anomalies, especially to unfamiliar destinations
* **Mass deletion**: bulk DELETE operations on PII tables outside of documented retention jobs
* **Unauthorized administrative access**: privilege escalation, root/sudo use outside documented break-glass procedures, IAM policy changes outside Terraform-driven workflows
* **Authentication anomalies**: credential stuffing, impossible-travel logins, account lockout spikes
* **Application-level anomalies**: query rate spikes against PII-bearing endpoints, unusual response sizes (a `GET /users/me` returning 50MB is not `users/me`)
* **Integrity violations**: unexpected modifications to audit logs, security configurations, or critical IAM resources
### Configuration substrates
The scanner reads:
* `prometheus/alerts/*.yml` and Alertmanager routing
* Datadog monitor exports (`monitors-*.json`)
* AWS CloudWatch alarm definitions (in IaC)
* GuardDuty / Security Hub / Macie configurations
* SIEM rule definitions (Splunk, Elastic, Sentinel, Wazuh)
* Application-level audit log emitters
For each required signal class above, the agent verifies at least one detection rule exists, and that the detection routes to a paging destination (PagerDuty, Opsgenie, on-call alias) rather than to a low-priority dashboard.
### Logging completeness
The detection only works if the underlying logs exist. The scanner verifies:
* PII-bearing endpoints emit access logs to a centralized destination
* The destination is append-only or has tamper-evidence (S3 with object lock, immutable bucket, write-only IAM policies)
* Log retention covers a window long enough for forensic reconstruction (typically 12-24 months for PII access logs)
* Logs themselves do not contain PII payloads (an audit log that records "user X accessed user Y's medical record" is correct; one that records the medical record's content is itself a confidentiality breach)
## Tier 2 checks: the runbook
`.compliance/incident_response.md` is read by the agent and validated against EDPB Guidelines 9/2022's phase structure.
### Phase 1: Detection and triage
The runbook documents:
* Who is on the initial response rotation
* The classification framework (confidentiality breach, integrity breach, availability breach, or some combination)
* The threshold criteria for declaring an incident vs. a non-incident security event
* The handoff to phase 2
A breach can be one or more of:
* **Confidentiality breach**: unauthorized disclosure of, or access to, personal data
* **Integrity breach**: unauthorized alteration of personal data
* **Availability breach**: accidental or unlawful destruction or loss of access to personal data
Loss of availability counts. A ransomware encryption that the controller cannot reverse is an availability breach even if no data is exfiltrated. The runbook should not treat ransomware as exclusively a security incident; it is also an Article 33 trigger.
### Phase 2: Assessment and containment
The runbook documents:
* The technical playbook for halting active data loss (revoke credentials, isolate hosts, block network egress)
* The forensic preservation requirements (snapshot affected systems before remediation, preserve volatile memory if relevant)
* The query playbook for answering: "Who was affected, and exactly what data?"
The "who and what" question is the hardest one and the one most often unanswerable in practice. The scanner specifically looks for query playbooks against the audit log destinations that can answer it; absence is a major finding.
### Phase 3: DPO and legal escalation
The runbook documents:
* Hardcoded routing to the DPO and external legal counsel within the first hours
* The decision authority for whether the threshold for Article 33 notification is met
* The decision authority for whether Article 34 high-risk communication to data subjects is required
### Phase 4: Supervisory authority notification
The runbook references:
* The competent supervisory authority for the controller (typically the lead authority under the One-Stop-Shop mechanism for cross-border processing)
* The notification template and required content per Article 33(3)
* The submission portal or contact for the authority
* The escalation if the 72-hour window cannot be met (Article 33(1) permits delayed notification "where the notification to the supervisory authority is not made within 72 hours, it shall be accompanied by reasons for the delay")
For Sweden, the competent authority is IMY (Integritetsskyddsmyndigheten). The runbook should reference the current IMY notification portal and the Swedish-language template.
### Phase 5: Data subject communication (Article 34)
When the breach is "likely to result in a high risk to the rights and freedoms of natural persons", communication directly to data subjects is required, in clear and plain language, "without undue delay".
The runbook should contain:
* Pre-drafted templates for common breach scenarios (credential breach, PII exfiltration, ransomware)
* Multilingual versions for jurisdictions in scope
* The threshold logic for high-risk determination, anchored in EDPB Guidelines 9/2022 and any DPA-specific guidance
* The communication channel decision tree (email, in-app, postal, public notice)
Article 34(3) lists exceptions: the data was encrypted to a level rendering it unintelligible, subsequent measures eliminated the high risk, or direct communication would involve disproportionate effort. The runbook should reference these exceptions explicitly so the decision is principled rather than ad hoc.
### Phase 6: Post-incident documentation
Article 33(5) requires the controller to document any personal data breach, regardless of whether it triggered notification, in a manner that enables the supervisory authority to verify compliance. The runbook documents the format and retention of the breach register.
## Common runbook failures the scanner catches
* Missing classification framework (the runbook treats all breaches as confidentiality breaches and ignores integrity / availability)
* DPO escalation path references a person who is no longer the DPO (drift; cross-reference against an HR/IDP source if available)
* Notification portal URL is deprecated or the regulator has updated the submission process
* Article 34 templates are present in only one language for a multi-jurisdiction service
* No defined query playbook for "who was affected and what data" - the runbook waves at "the security team will determine the scope" without specifying how
* No documented threshold logic for high-risk determination, leaving phase 5 to ad hoc judgment
## Processor notification
Article 33(2) requires processors to notify their controllers "without undue delay" upon becoming aware. The 72-hour clock for the controller starts when the controller is informed by the processor (subject to interpretation; the EDPB position is that the controller should be informed as quickly as possible to preserve the 72-hour window).
If the repository operates as a processor for one or more controllers, the runbook documents:
* The notification SLA to controllers (typically 24 hours or sooner, set in the DPA)
* The communication channel per controller relationship
* Sample notification content
The scanner verifies the existence of these elements when the codebase has indicators of processor operation (DPA templates in `.compliance/`, multi-tenant architecture, service-level agreements naming controller customers).
@@ -0,0 +1,150 @@
# Canonical Document Set
A GDPR-aware repository contains a `.compliance/` directory holding machine-readable versions of the privacy artifacts. Treating privacy documentation as code is the precondition for every Tier 2 agentic check; without it, the auditor has no policy substrate against which to validate codebase reality.
## Layout
```
.compliance/
├── ropa.yaml # or ropa.json - Record of Processing Activities
├── dpia_inventory/
│ ├── ml_credit_scoring.md # one DPIA per high-risk operation
│ ├── biometric_login.md
│ └── customer_segmentation.md
├── dsar_runbook.md # technical playbook for Articles 15/16/17/20
├── transfers/
│ ├── tia_aws_us_east_1.md # one TIA per restricted transfer
│ ├── scc_module_2_aws.pdf # executed SCCs, by module
│ └── transfers_inventory.yaml # machine-readable index
├── incident_response.md # 72-hour breach runbook
├── privacy_policy.md # forward-facing notice
└── consent_mappings.yaml # cookie/consent state to processing operation map
```
The structure is a convention, not a regulation. The agent should be tolerant of variants (`/.privacy/`, `/docs/compliance/`, etc.) but must locate and validate equivalents of all the artifacts above.
## Per-artifact validation rules
### `ropa.yaml` - Article 30
**Required schema**:
```yaml
processing_activities:
- id: "string"
name: "string"
purpose: "string"
lawful_basis: "consent | contract | legal_obligation | vital_interests | public_task | legitimate_interests"
data_categories: ["fideslang-label", ...]
data_subjects: ["fideslang-label", ...]
recipients: ["string", ...] # internal services + external processors
retention_period: "ISO-8601 duration"
international_transfers:
- destination: "country-code"
safeguard: "adequacy | scc_module_X | bcr | derogation_art_49"
tia_ref: "path/to/transfers/tia_*.md"
security_measures: ["string", ...]
```
**Tier 1 checks**: schema validity; every `data_categories` and `data_subjects` entry resolves to a Fideslang label; every `tia_ref` points to an existing file under `.compliance/transfers/`.
**Tier 2 checks**: every flow in the Privado-generated data flow graph appears as a `processing_activities` entry, and vice versa. The agentic prompt for this is in `agentic-prompts.md`.
**Common failure mode**: the RoPA is hand-maintained and drifts the moment a developer adds a new third-party SDK or a new database column. Drift is the rule, not the exception. The scanner exists primarily to catch this.
### `dpia_inventory/` - Article 35
Each file is a DPIA for a single high-risk processing operation. Structure follows the EDPB DPIA template (April 2026 update):
1. Systematic description of envisaged processing operations and purposes
2. Assessment of necessity and proportionality
3. Assessment of risks to data subjects' rights and freedoms
4. Measures envisaged to address the risks (including safeguards, security measures, mechanisms to ensure protection)
5. Date and signature of the DPO consultation
6. Where applicable, prior consultation with the supervisory authority (Art. 36)
**Tier 1 checks**: file presence per high-risk trigger detected in code; section headers match the EDPB template.
**Tier 2 checks**: the "measures envisaged" section names specific technical controls that the agent can then verify exist in code (encryption keys managed by service X, access logged to service Y, etc.). If the DPIA promises a control that the codebase does not implement, the scanner flags a DPIA-vs-implementation drift.
### `dsar_runbook.md` - Articles 15-22
Documents the technical mechanism by which each data subject right is satisfied. For each right, expected content:
* Endpoint(s) involved
* Authentication mechanism
* Identity verification procedure (proof-of-identity above the API auth layer)
* Microservices queried (with explicit join logic for distributed PII)
* SLA against the 30-day Article 12(3) window
* Escalation path for ambiguous requests (e.g., requests touching ongoing legal proceedings, conflicting data subjects)
**Tier 2 check**: the agent reads the runbook, extracts the named endpoints, and verifies each exists in the API layer with the documented authentication and behavior.
### `transfers/` - Articles 44-49
`transfers_inventory.yaml` is the machine-readable index:
```yaml
transfers:
- destination_country: "US"
destination_service: "aws-us-east-1"
fideslang_data_categories: ["user.contact.email", "user.preferences"]
safeguard: "scc_module_2"
scc_executed_date: "2024-03-15"
scc_artifact: "transfers/scc_module_2_aws.pdf"
tia_artifact: "transfers/tia_aws_us_east_1.md"
supplementary_measures:
- "encryption in transit (TLS 1.3)"
- "encryption at rest with CMK held in EU"
- "pseudonymization of direct identifiers prior to transfer"
```
Detailed Schrems II evaluation logic is in `transfers-schrems-ii.md`.
### `incident_response.md` - Articles 33 and 34
Detailed phase structure and EDPB Guidelines 9/2022 alignment in `breach-notification.md`. The high-level required phases:
1. Detection and triage (confidentiality / integrity / availability classification)
2. Assessment and containment
3. DPO and legal escalation paths
4. Supervisory authority notification (within 72 hours of awareness)
5. Data subject communication (Article 34, when high risk)
6. Post-incident review and Article 33(5) documentation
### `privacy_policy.md` - Articles 12, 13, 14
The user-facing notice. The agent verifies:
* Every Fideslang `data_category` actually processed by the codebase appears in the policy
* Every `data_use` actually invoked appears in the policy as a stated purpose
* Every recipient (third-party SDK, processor) appears in the policy
* Retention periods named in the policy match the retention enforcement in code
* Lawful basis named in the policy matches the basis declared in `ropa.yaml`
**Common failure mode**: the legal team updates the privacy policy on a quarterly cadence; the engineering team adds a new analytics SDK on a Tuesday afternoon. The policy is now lying to data subjects. The scanner catches this within the same PR.
### `consent_mappings.yaml`
Maps cookie / consent state to the processing operations they unlock. Used by the agent to validate frontend consent logic against backend behavior:
```yaml
consent_categories:
strictly_necessary:
fideslang_data_uses: ["provide.service.operations"]
consent_required: false
analytics:
fideslang_data_uses: ["improve.system", "improve.system.analytics"]
consent_required: true
cookie_names: ["_ga", "_gid"]
marketing:
fideslang_data_uses: ["marketing.advertising.first_party.contextual"]
consent_required: true
```
The agent uses this to check, for example, that no script tagged as `marketing` initializes before the user has accepted the marketing consent category.
## Drift as the primary failure mode
In every category above, the dominant failure mode is not the absence of the artifact but its silent divergence from code reality. A scanner that only checks for file existence is performing security theater. Every Tier 2 check in this file is, fundamentally, a drift check.
The agent's mental model: each artifact is a *contract* between legal and engineering, expressed in code-adjacent form. Drift is contract breach.
@@ -0,0 +1,59 @@
# Cross-Framework Mapping
The Secure Controls Framework (SCF) operates as a Living Control Set (LCS) and provides Set Theory Relationship Mapping (STRM) between regulatory regimes. The SCF translates the GDPR into 1,400+ discrete technical controls across 33 domains, and exports its mappings as NIST OSCAL JSON. The scanner ingests this OSCAL artifact to emit multi-framework evidence tags from a single technical check.
When a GDPR violation is detected, the agent should output the GDPR article *and* the equivalent identifier in NIST 800-53 Rev 5, ISO/IEC 27001:2022 Annex A, SOC 2 TSC, and CIS Controls v8. This serves the typical compliance team that operates against multiple regimes simultaneously and reduces remediation duplication.
## The crosswalk table
The mapping below is the operational core. The agent should treat it as authoritative for the GDPR articles enumerated; for less common articles, fall back to the SCF OSCAL export.
| GDPR | Verifiable technical objective | NIST 800-53 R5 | ISO/IEC 27001:2022 Annex A | SOC 2 TSC | CIS v8 |
|------|---------------------------------|----------------|----------------------------|-----------|--------|
| Art. 5(1)(f), Art. 32 | Encryption at rest (AES-256) and in transit (TLS 1.2+); key management | SC-8 (Transmission Confidentiality and Integrity), SC-28 (Protection of Information at Rest), SC-12 (Cryptographic Key Establishment) | A.8.24 (Use of cryptography) | CC6.1 (Logical Access), CC6.6 (Boundary Protection), CC6.7 (Data Transmission) | 3.10, 3.11 (Data protection) |
| Art. 5(1)(c) | Data minimization in API schemas and data collection | PT-2 (Authority to Process), PT-3 (PII Processing Purposes) | A.5.34 (Privacy and protection of PII) | P3.1, P3.2 (Privacy purpose) | 3.1 (Data management process) |
| Art. 5(1)(e), Art. 17 | Storage limitation, retention enforcement, irreversible erasure | SI-12 (Information Management and Retention), MP-6 (Media Sanitization) | A.5.10 (Acceptable use), A.8.10 (Information deletion) | P4.2 (Privacy retention), P4.3 (Privacy erasure) | 3.4 (Data retention), 3.5 (Disposal) |
| Art. 25 | Privacy by design and default; least privilege; secure defaults | AC-2 (Account Management), AC-3 (Access Enforcement), AC-6 (Least Privilege), CM-6 (Configuration Settings), CM-7 (Least Functionality) | A.5.15 (Access control), A.8.2 (Privileged access rights), A.8.9 (Configuration management) | CC6.1, CC6.3 (Logical and Physical Access) | 3.3 (Access control list), 4.1 (Secure configuration) |
| Art. 30 | Records of Processing Activities; data inventory; data flow mapping | PM-5 (System Inventory), PT-2, PT-3, CM-8 (System Component Inventory) | A.5.9 (Inventory of information and other associated assets), A.5.34 | CC6.1 (Information Asset Inventory), P3.1 | 1.1 (Asset inventory), 3.2 (Data inventory) |
| Art. 32, Art. 5(1)(f) | Continuous logging, integrity monitoring, secret scanning | AU-2 (Event Logging), AU-12 (Audit Generation), SI-7 (Software Integrity), IA-5 (Authenticator Management) | A.8.15 (Logging), A.8.16 (Monitoring activities), A.5.17 (Authentication information) | CC7.1, CC7.2 (System Operations) | 8.1, 8.2 (Audit log management) |
| Art. 33, Art. 34 | Incident detection, response, breach notification within 72 hours | IR-4 (Incident Handling), IR-6 (Incident Reporting), IR-8 (Incident Response Plan) | A.5.24 (Information security incident management planning), A.5.25 (Assessment and decision on information security events), A.5.26 (Response to information security incidents) | CC7.3 (Incident detection), CC7.4 (Incident response) | 17.1-17.9 (Incident response) |
| Art. 35 | DPIA for high-risk processing; risk assessment | RA-3 (Risk Assessment), RA-8 (Privacy Impact Assessments), PT-7 (Specific Categories of PII) | A.5.30 (ICT readiness), A.5.34 | P1.1 (Privacy notice), P6.1-P6.6 (Privacy disclosure and notification) | n/a (CIS does not address DPIA) |
| Art. 15-21 | Data subject rights endpoints; access, rectification, erasure, portability | PT-4 (Consent), PT-5 (Privacy Notice), PT-6 (System of Records Notice), PM-21 (Accounting of Disclosures) | A.5.34, A.8.10 (Information deletion), A.8.11 (Data masking) | P4.1 (Privacy access), P4.3 (Privacy erasure), P5.1 (Privacy disclosure) | n/a |
| Art. 44-49 | International transfers, SCCs, TIAs, supplementary measures | SR-3 (Supply Chain Controls and Processes), AC-21 (Information Sharing), CA-3 (Information Exchange) | A.5.20 (Addressing information security within supplier agreements), A.5.21 (Managing information security in the ICT supply chain), A.5.31 (Legal, statutory, regulatory and contractual requirements) | CC9.2 (Vendor Risk), C1.1 (Confidentiality) | 15.1-15.7 (Service provider management) |
| Art. 32 | Backup and recovery; ability to restore | CP-9 (System Backup), CP-10 (System Recovery and Reconstitution) | A.8.13 (Information backup), A.5.30 (ICT readiness for business continuity) | A1.2 (Availability backup), A1.3 (Recovery testing) | 11.1-11.5 (Data recovery) |
## SCF OSCAL ingestion
The full SCF OSCAL export is available from the SCF's GitHub. The scanner should:
1. Pin a specific SCF release version in the build pipeline (the SCF is a Living Control Set; mappings change).
2. Parse the OSCAL JSON to build an in-memory map: `gdpr_article -> [scf_control_id, ...] -> [external_framework_control_id, ...]`.
3. When a check fails, the scanner traverses from the GDPR article through the SCF control to all linked external framework controls, and emits the full set in the output.
## Output format
A single technical violation should produce a finding shaped like:
```json
{
"rule_id": "gdpr-32-unencrypted-storage",
"severity": "high",
"file": "infrastructure/terraform/s3.tf",
"line": 42,
"evidence": "aws_s3_bucket.user_uploads has server_side_encryption_configuration absent",
"framework_mappings": {
"gdpr": ["Art. 32(1)(a)"],
"nist_800_53_r5": ["SC-28"],
"iso_27001_2022": ["A.8.24"],
"soc2_tsc": ["CC6.1", "CC6.6"],
"cis_v8": ["3.11"]
},
"remediation": "Add server_side_encryption_configuration block with sse_algorithm = \"AES256\" and (for restricted-transfer destinations) a CMK ARN."
}
```
This structure satisfies a vendor risk team running ISO 27001, an enterprise customer demanding SOC 2 evidence, and a European DPA simultaneously, from a single repository scan.
## DPMP reference
The SCF also incorporates the Data Privacy Management Principles (DPMP), which organize 86 individual privacy principles into 11 domains and map them to 31 global privacy frameworks. When the customer organization operates outside the EU but processes EU data subject information, the DPMP layer is what extends GDPR-aligned controls to PIPEDA, LGPD, CCPA/CPRA, and other regimes. The mapping logic is identical: GDPR article -> SCF control -> DPMP principle -> destination-framework control identifier.
@@ -0,0 +1,145 @@
# DSAR API Patterns
Articles 15-22 require the data controller to fulfill data subject requests within 30 days (Article 12(3), extendable by two months for complex cases). The scanner verifies that the repository implements specific endpoint patterns and that the OpenAPI contracts enforce purpose limitation.
## Required endpoints
The agent searches for endpoints satisfying the access (Art. 15), erasure (Art. 17), and portability (Art. 20) rights. Rectification (Art. 16) is typically satisfied through the existing user-profile-update endpoint and is verified differently. Restriction (Art. 18) and objection (Art. 21) usually correspond to flag-flip operations on processing consent.
### Access (Art. 15)
```
GET /api/v1/privacy/user/data
Authorization: Bearer <subject_token>
Accept: application/json
```
**Behavior**: aggregates and returns a unified profile of all personal data the controller holds about the requesting subject across the entire microservice architecture.
**Verification rules**:
* The endpoint exists and is registered in the OpenAPI spec.
* The controller method invokes a documented aggregation function (not a single-table query).
* Response schema includes every Fideslang `data_category` documented in `.compliance/ropa.yaml` for the subject's category.
* Identity verification beyond the API auth layer is documented in `.compliance/dsar_runbook.md` (proof-of-identity requirement, since a stolen access token must not enable a full data dump).
### Erasure (Art. 17)
```
DELETE /api/v1/privacy/user/data
Authorization: Bearer <subject_token>
```
**Behavior**: triggers irreversible deletion or cryptographic shredding across all data stores. Returns a job identifier for asynchronous processing tracking.
**Verification rules** (the highest-frequency violation site):
* The handler does NOT execute `UPDATE ... SET is_deleted = true` (or equivalent ORM soft-delete). The agent traces the controller statically.
* The handler invokes a documented hard-delete or cryptographic-shredding function on each data store enumerated in the RoPA.
* Legitimate retention overrides (tax law, AML) are documented in the DPIA or RoPA, with the specific fields and retention bases enumerated. The handler honors these overrides via specific conditional logic (not a general soft-delete fallback).
* The job state is auditable and the controller can demonstrate completion within 30 days.
### Portability (Art. 20)
```
GET /api/v1/privacy/user/export
Authorization: Bearer <subject_token>
Accept: application/json | text/csv
```
**Behavior**: serializes the subject's data into a structured, commonly used, machine-readable format. JSON or CSV are the typical defaults; XML or other open formats are also acceptable.
**Verification rules**:
* The response is structured (not a PDF or HTML rendering of profile data).
* The schema is the same shape as Article 15 for the data the subject provided to the controller (Article 20(1) is narrower in scope than Article 15; only data the subject has provided is portable).
### Rectification (Art. 16)
Typically the existing `PUT /api/v1/users/me` or equivalent. The verification check is that the same endpoint also exists for non-self-service modifications when the subject's request must be processed asynchronously.
### Restriction and objection (Art. 18, Art. 21)
```
POST /api/v1/privacy/user/restrict
POST /api/v1/privacy/user/object
```
**Behavior**: flag-flip operations that alter downstream processing. The verification check is that the flag is *honored* by the relevant processing operations - the agent traces from the flag store to the processing operations registered against it in the RoPA.
## OpenAPI Schema validation
Purpose limitation (Article 5(1)(b)) is mechanically enforceable through schema validation. Two rules:
### `additionalProperties: false`
Every data-ingestion request schema must set `additionalProperties: false`. Without it, the schema admits unbounded extraneous fields that bypass the data minimization principle.
```yaml
components:
schemas:
CreateUserRequest:
type: object
additionalProperties: false # required
required: [email, locale]
properties:
email: { type: string, format: email, x-fideslang: "user.contact.email" }
locale: { type: string, x-fideslang: "user.preferences" }
```
### Fideslang annotation alignment with declared scope
The agent reads the OAuth scope required by the endpoint (e.g., `customer_support`) and validates against the response schema's Fideslang annotations. An endpoint scoped `customer_support` must not return fields annotated `user.payment.credit_card_number` or `user.behavior.browsing_history` - those scopes are not commensurate with customer support purposes.
## RBAC and identity verification
DSAR endpoints are high-value targets. An attacker with a stolen access token effectively has the keys to a complete data dump. Two layers of defense are required:
### API-layer authentication
OAuth 2.0 or OpenID Connect with strict scope. The token must carry a scope specific to DSAR operations (e.g., `dsar:self`); a general `user` scope is insufficient.
### Identity proofing
The DSAR runbook documents a proof-of-identity step beyond the access token. Common patterns:
* Email-link confirmation to the registered address with a short TTL
* Multi-factor reauthentication
* Knowledge-based verification questions (less preferred)
* For high-sensitivity contexts, a manual review step
The agent verifies that the runbook documents this and that the endpoint's controller logic invokes the documented verification function.
## Third-party PII redaction
Article 15(4) limits the right of access to the extent that providing data would adversely affect the rights and freedoms of others. In practice: a chat log including messages from other users, a shared document with co-author identities, an organization context with peer information.
The agent verifies that the response builder for `GET /api/v1/privacy/user/data` invokes a documented redaction function over fields that may contain third-party PII. The redaction function must:
* Replace direct identifiers (other users' names, email addresses) with anonymous placeholders
* Strip metadata that could re-identify the third party (user IDs, session IDs)
* Preserve the structural relationship for the requesting subject's understanding (so a chat log remains coherent without naming the other party)
The check is partly Tier 1 (the function exists and is invoked) and partly Tier 2 (the redaction is sufficient given the actual third-party data shapes in scope).
## Asynchronous job tracking
Erasure and large-scale exports cannot complete synchronously. The agent verifies the existence of:
* A job creation endpoint that returns a job identifier
* A status endpoint to poll job completion
* A retention period for the job artifact (export downloads must expire; the export file itself is sensitive PII)
* An audit log of job initiation, progression, and completion, with each entry tied to the requesting subject
## Idempotency and replay
DSAR endpoints should be idempotent at the request level. A subject who issues an erasure request twice in error should not have the request re-queued or duplicated. The agent checks for an idempotency key pattern in the controller, typically a request ID or a content-derived hash.
## Common failure modes by article
| Article | Common DSAR endpoint failure |
|---------|------------------------------|
| Art. 15 | Endpoint returns only the primary user table; misses records in event store, analytics warehouse, message queues, third-party processors |
| Art. 17 | Soft-delete masquerading as erasure |
| Art. 17 | Backups not addressed; deleted data remains recoverable indefinitely |
| Art. 20 | Format is not machine-readable (PDF), or limited to data the controller derived rather than data the subject provided |
| Art. 18 | Flag flipped but downstream processors do not consult the flag |
| Art. 21 | Marketing systems not wired to the objection flag; opt-out is not honored |
The scanner should emit specific findings against each of these patterns when detected.
@@ -0,0 +1,125 @@
# GDPR Articles - Operative Subset
The articles below form the verifiable core of an automated GDPR scanner. Each entry gives the legal scope, the technical intent for an automated auditor, the verification tier, and the concrete artifacts the scanner inspects.
The agent must reference articles by exact identifier in its output. Multi-framework crosswalks for each article live in `cross-framework-mapping.md`.
## Article 5 - Principles relating to processing of personal data
**Scope**: Lawfulness, fairness, transparency, purpose limitation, data minimization, accuracy, storage limitation, integrity and confidentiality (the "security principle"), and accountability.
**Technical intent**: Verify that data collection mechanisms are scoped strictly to stated purposes (no over-collection), that retention policies exist and are mechanically enforced, and that integrity/confidentiality is upheld through cryptographic primitives.
**Tier**: Mixed. Tier 1 for retention scripts, schema minimality, and encryption. Tier 2 for purpose limitation against stated business purpose in OpenAPI documentation.
**Artifacts inspected**:
* OpenAPI request and response schemas (over-collection detection)
* Cron jobs and lifecycle policies for storage (retention enforcement)
* ORM column definitions and IaC encryption flags (integrity/confidentiality)
* `.compliance/privacy_policy.md` claims vs. actual fields collected (transparency)
## Article 6 - Lawfulness of processing
**Scope**: Establishes the six lawful bases (consent, contract, legal obligation, vital interests, public task, legitimate interests).
**Technical intent**: For each processing operation, verify that the declared lawful basis is structurally compatible with the operation. Consent-based operations require evidence of opt-in capture; legitimate-interest operations require a documented LIA (Legitimate Interests Assessment).
**Tier**: Tier 2. The legal basis is declared in `.compliance/ropa.yaml` per processing activity; the agent reads the declaration and validates it against the code path.
**Common violation**: invoking a sensitive API (camera, geolocation, contacts) without a preceding consent check. From the document, an Android example: `manager.openCamera(cameraId, stateCallback, null)` with no preceding conditional gating consent state.
## Article 9 - Special categories of personal data
**Scope**: Prohibits processing of racial/ethnic origin, political opinions, religious beliefs, trade union membership, genetic data, biometric data, health data, and data concerning sex life or sexual orientation. Lifted only by one of the Article 9(2) exceptions (explicit consent, vital interests, substantial public interest, etc.).
**Technical intent**: Detect any code path or schema field annotated as a special category and require an explicit Article 9(2) basis declaration in the RoPA, plus matching consent capture infrastructure if the basis is 9(2)(a).
**Tier**: Tier 1 detection of the field via Fideslang labels (`user.health.*`, `user.biometric.*`, `user.genetic.*`); Tier 2 validation of the declared 9(2) basis.
**Hard rule**: an endpoint with Fideslang `data_use: marketing.advertising.third_party` and any data category under `user.health.*`, `user.biometric.*`, or `user.genetic.*` is an immediate, non-suppressible Article 9 violation. Marketing is never a valid 9(2) basis.
## Articles 15-22 - Data subject rights
**Scope**: Access (15), rectification (16), erasure / right to be forgotten (17), restriction (18), portability (20), objection (21), automated decision-making (22).
**Technical intent**: Verify the existence of dedicated, RBAC-protected API endpoints satisfying each right within 30 days. Detailed REST patterns and JSON Schema rules are in `dsar-api-patterns.md`.
**Tier**: Tier 1 for endpoint existence and method shape; Tier 2 for tracing the execution path of `DELETE /api/v1/privacy/user/data` to verify hard deletion (or cryptographic shredding) rather than a soft-delete boolean toggle.
**Article 17 anti-pattern (highest-frequency violation)**: implementing erasure as `UPDATE users SET is_deleted = 1`. This is non-compliant. The right to erasure requires hard deletion or mathematically irreversible anonymization. The agent must trace the controller method statically and flag any soft-delete pattern.
**Article 22**: see `limitations.md`. The scanner can detect ML model deployment; it cannot adjudicate whether human review is "meaningful".
## Article 25 - Data protection by design and by default
**Scope**: Embedding privacy-enhancing technologies (PETs) into system architecture from the earliest design stages. Default settings must be the most privacy-preserving available.
**Technical intent**: Audit configuration files for secure defaults (minimum necessary permissions, tracking disabled by default, opt-in rather than opt-out flows). Verify IaC implements least-privilege architectures.
**Tier**: Tier 1 across IaC (Terraform, CloudFormation, Pulumi) for IAM policy granularity, default-deny network rules, default-encrypted storage, and disabled telemetry.
**Common violations**:
* `Access-Control-Allow-Origin: *` on endpoints serving authenticated PII
* AWS S3 buckets with `public-read` default ACL
* Mobile app `AndroidManifest.xml` requesting `ACCESS_FINE_LOCATION` or `READ_CONTACTS` without functional necessity tied to declared purpose
* Missing `Strict-Transport-Security`, `Content-Security-Policy`, `X-Content-Type-Options` headers
## Article 30 - Records of processing activities (RoPA)
**Scope**: Comprehensive documentation of personal data flows, categories of data subjects, processing purposes, recipients (including third parties), retention periods, and (for international transfers) the safeguards in place.
**Technical intent**: Validate the existence and structural integrity of `.compliance/ropa.yaml`, then reconcile its declared flows against the data flow graph generated by Privado. Any flow appearing in code but not in the RoPA, or vice versa, is a synchronization violation.
**Tier**: Tier 1 for schema validation; Tier 2 for the RoPA-vs-actual-flow reconciliation.
**Drift detection prompt** (excerpt; full version in `agentic-prompts.md`): the agent receives the parsed RoPA and the Privado JSON, and is asked to enumerate flows present in one but not the other. Drift directly violates Art. 30(1) and is a precondition for almost every downstream GDPR violation.
## Article 32 - Security of processing
**Scope**: Appropriate technical and organizational measures, with explicit examples: pseudonymization, encryption, ability to ensure ongoing confidentiality / integrity / availability / resilience, ability to restore access in a timely manner after an incident, and a process for regularly testing those measures.
**Technical intent**: The most deterministically scannable article. Hardcoded secrets, weak cryptography, plaintext transmission, unencrypted storage, missing backup configurations, and absent restore procedures are all Tier 1.
**Tier**: Tier 1 for nearly everything.
**Detection rules**:
* Secret scanning via GitLeaks or Semgrep against the full Git history (not just HEAD)
* TLS configuration parsing rejecting SSLv3, TLS 1.0, TLS 1.1, and weak cipher suites
* IaC parsing for `encrypted: true` on storage resources, with KMS key ARN validation
* ORM annotation scanning for sensitive entities (`SocialSecurityNumber`, `HealthRecord`, anything Fideslang-labeled `user.payment.*` or `user.health.*`) without encryption decorators
* Backup job verification (cron presence + retention period + restore drill schedule)
## Articles 33 and 34 - Personal data breach notification
**Scope**: Article 33 imposes a 72-hour notification window to the supervisory authority from the moment of "awareness". Article 34 requires direct communication to data subjects when the breach is likely to result in a "high risk" to their rights and freedoms. EDPB Guidelines 9/2022 (Version 2.0, March 2023) define the operative thresholds.
**Technical intent**: Verify that the repository's monitoring, alerting, and runbook infrastructure can technically support the 72-hour window. The runbook itself is a Tier 2 artifact; the alerting infrastructure is Tier 1.
**Tier**: Tier 1 for monitoring configurations (Prometheus alert rules, Datadog monitors, CloudWatch alarms, OpenSearch detectors); Tier 2 for `.compliance/incident_response.md` evaluation against EDPB Guidelines 9/2022 phases.
Full decomposition in `breach-notification.md`.
## Article 35 - Data protection impact assessment (DPIA)
**Scope**: Required for any processing "likely to result in a high risk to the rights and freedoms of natural persons", with explicit triggers including systematic and extensive automated profiling, large-scale processing of special categories, and systematic monitoring of publicly accessible areas.
**Technical intent**: Identify code patterns matching high-risk criteria (ML profiling, biometric processing, large-scale special-category handling, public-area monitoring) and require a corresponding DPIA in `.compliance/dpia_inventory/` structured against the EDPB DPIA template (April 2026 update).
**Tier**: Tier 1 for the trigger detection (ML library imports, biometric API calls); Tier 2 for the DPIA artifact structural and content validation.
**Trigger heuristics**:
* Imports of `tensorflow`, `pytorch`, `sklearn`, `xgboost`, `lightgbm` in production paths
* Calls to facial recognition, voice analysis, or fingerprinting APIs
* Cross-database joins producing comprehensive individual profiles
When triggered without a corresponding DPIA, the scanner blocks the pipeline.
## Articles 44-49 - Transfers to third countries
**Scope**: Prohibits transfers of personal data outside the EEA absent an adequacy decision (Art. 45), appropriate safeguards (Art. 46, including the 2021 modular SCCs and Binding Corporate Rules), or a derogation (Art. 49). Schrems II (CJEU C-311/18) requires that even with SCCs, the controller must perform a Transfer Impact Assessment and apply supplementary measures where the destination's law undermines EU protection.
**Technical intent**: Trace data flows to external sinks, resolve their geographic location, classify as restricted transfer if non-EEA, and validate corresponding SCC + TIA + supplementary measures in `.compliance/transfers/`.
**Tier**: Tier 1 for IP/hostname geolocation and TIA-mandated technical measure verification (CMK enforcement, EU-resident key custody); Tier 2 for SCC module selection correctness (Module 1 controller-to-controller, Module 2 controller-to-processor, Module 3 processor-to-processor, Module 4 processor-to-controller).
Full decomposition in `transfers-schrems-ii.md`.
@@ -0,0 +1,118 @@
# Limits of Automation
The honest scope of what an automated GDPR scanner cannot do. A scanner that pretends otherwise is itself a compliance risk, because it produces a false sense of completeness while the actual high-stakes legal questions go unaddressed.
This document enumerates the categories of GDPR question that fall outside automation's reach, and prescribes how the scanner should behave when it encounters them: surface, halt, escalate, never silently pass.
## Article 22: automated decision-making with legal or similarly significant effects
Article 22 protects individuals from decisions based "solely" on automated processing that produce "legal effects" or "similarly significantly affect" them. Hiring decisions, credit scoring, insurance underwriting, parole risk assessment, eligibility for state benefits - these are paradigm cases. Recommender systems and ad targeting are usually not, but can become so depending on context (e.g., serving high-stakes ads only to certain demographic clusters).
What the scanner can do:
* Detect the presence of an ML model in a production code path
* Identify the model's role in a decision pipeline (advisory, gating, automated)
* Verify a DPIA exists for the operation if it is high-risk
* Extract from the DPIA the declared mitigation measures and check them against code
What the scanner cannot do:
* Adjudicate whether the decision produces "legal effects" or "similarly significantly affects" the data subject. This is fact-specific legal interpretation.
* Adjudicate whether a human in the loop constitutes "meaningful human involvement" or is a rubber stamp. The CPPA's recent rules and the EDPB's guidance both emphasize this distinction, but it cannot be measured statically. A reviewer who clicks "approve" on 1,000 cases per day is rubber-stamping; a reviewer who reads each case takes 10 minutes per case. The scanner cannot tell from code which is happening.
* Adjudicate whether the operation falls within the Article 22(2) exceptions (necessary for contract, authorized by law, based on explicit consent).
What the scanner does:
* When it detects a model in a pipeline that touches Fideslang categories `user.financial.*`, `user.employment.*`, `user.legal.*`, `user.health.*`, `user.behavior.children.*`, or `user.demographic.protected_class`, it halts the pipeline.
* It emits a structured escalation: "Article 22 review required. Detected model at <file:line>. Decision pipeline shape: <inferred>. DPIA present: yes/no. Human review step present in code: yes/no. The following questions cannot be answered automatically: [whether the decision produces legal effects, whether human involvement is meaningful, whether an Article 22(2) exception applies]. Resolution requires DPO sign-off."
* It does not emit a pass.
## Article 9: special category processing under "substantial public interest"
Article 9(2)(g) permits processing of special category data when "necessary for reasons of substantial public interest, on the basis of Union or Member State law". This is the basis under which research databases, public health systems, and certain national security operations process health, biometric, or other special category data.
What the scanner cannot do:
* Adjudicate whether a stated public interest is "substantial".
* Verify the existence and current status of the Union or Member State legal basis claimed.
* Weigh the public interest against the data subjects' rights (the proportionality test).
What the scanner does:
* When it detects special category processing with declared basis `art_9_2_g_substantial_public_interest`, it requires:
- A reference in the DPIA to the specific Union or Member State law providing the basis
- A reference to the proportionality assessment
- A reference to safeguards specific to special category processing
* It does not adjudicate the substance, but it does verify the artifacts that an audit would need to evaluate the substance exist.
## Risk-based provisions: "appropriate measures", "high risk", "undue delay"
The GDPR is replete with standards rather than rules. "Appropriate technical and organizational measures" (Art. 32). "High risk to the rights and freedoms of natural persons" (Art. 35, Art. 34). "Without undue delay" (Art. 12, Art. 33). "Likely to result in" (Art. 35, Art. 34).
The scanner cannot adjudicate these standards directly. It can:
* Encode common heuristics as defaults (TLS 1.2+ is appropriate; TLS 1.0 is not; AES-256 is appropriate; DES is not)
* Verify that a DPIA explicitly addresses the risk question
* Verify that the runbook defines operational thresholds rather than leaving them to ad hoc judgment
It cannot:
* Decide whether a specific implementation is "appropriate" in a contested case
* Decide whether a specific breach is "high risk" without DPO judgment
* Decide whether a specific delay was "undue"
When the scanner cannot decide, it should not pretend to. A "needs DPO review" finding is a complete answer.
## Cross-border legal landscape changes
Adequacy decisions are issued, suspended, and revoked. The CJEU rules on standing concepts (Schrems II invalidated the Privacy Shield in a single decision). National DPA enforcement priorities shift.
The scanner pins reference data (the adequacy decision list, the SCC module text, the EDPB guideline versions) at a specific date. It cannot self-update with currency. The scanner emits a soft warning when its reference data is older than 90 days and a hard requirement when it is older than 365 days, prompting the operator to update.
The scanner does not pretend to know about cases or guidelines published after its reference date.
## Out-of-repository organizational facts
Several Tier 3 facts cannot be verified from code at all. The scanner expects evidence pointers but cannot validate the substance:
* DPO competence and independence (Art. 38, 39)
* Effective implementation of training programs
* Real-world execution of contractual safeguards (a signed DPA does not guarantee the processor honors it)
* Physical security measures
* Governance independence between data controller and security functions
For each of these, the scanner verifies that an evidence pointer exists pointing somewhere in principle accountable. It does not verify that the underlying claim is true.
## The boundary the scanner enforces
The scanner's contract with its operator is precise:
| Category | Scanner verdict |
|----------|-----------------|
| Tier 1 violation present | Block the build, specific finding |
| Tier 2 violation present | Block the build, agentic explanation |
| Tier 3 evidence pointer missing | Block the build, request evidence URI |
| Article 22 / Article 9(2)(g) / risk-based provision implicated | Block the build, escalate to DPO |
| Reference data is stale | Soft warning to operator, hard block above threshold |
| Suppression with valid justification | Pass with logged exception |
| Everything checks out | Pass |
There is no "pass with concerns" verdict. The scanner either passes or escalates; concerns become concrete blocks or concrete escalations. This is what makes the scanner useful. A scanner that emits ambient anxiety without actionable next steps becomes background noise that engineers route around.
## Why the limits matter
Two failure modes haunt automated compliance:
**False confidence**: the scanner passes, the engineer ships, the regulator later finds a violation in territory the scanner never covered. The organization then claims good-faith reliance on tooling, which the regulator does not credit because the violation was in a category any competent privacy program would have addressed manually.
**Alert fatigue**: the scanner emits dozens of low-confidence warnings on every PR. Engineers learn to ignore them or to suppress them en masse. The signal-to-noise ratio collapses, and a real violation passes unnoticed.
The remedy for both is the same: be honest about what the scanner does and does not check. Tier 1 findings are mechanical and high-confidence; emit them with conviction. Tier 2 findings are agentic and require human review; mark them as such. Tier 3 facts are out of repo; collect pointers, do not pretend to verify substance. And for the genuinely subjective questions (Art. 22, substantial public interest, undue delay), do not emit a verdict at all - escalate.
The scanner's value is in the boundary it draws between "this is mechanically wrong" and "this requires legal judgment". Both halves are necessary; both halves must be honestly named.
## Reference: the human-in-the-loop pattern
The deployment pattern this skill is built for is not "automated GDPR compliance". It is "automated elimination of routine compliance errors, with structured escalation of substantive questions to a Data Protection Officer".
The DPO uses the scanner's outputs to:
* Spend zero time on Tier 1 findings (they are auto-blocked, the engineer fixes them, no DPO involvement)
* Spend modest time auditing Tier 2 findings (the agentic reasoning is a draft; the DPO reviews and accepts or rejects)
* Spend the bulk of their time on the substantive questions the scanner correctly refuses to answer (Article 22, Article 9 substantial public interest, proportionality)
This division of labor is what justifies the engineering investment. The scanner makes the DPO more effective by removing routine work from their plate; it does not replace the DPO.
A scanner sold as a DPO replacement is being mis-sold. A scanner sold as a DPO force multiplier is being sold honestly.
@@ -0,0 +1,133 @@
# Tool Orchestration
Build the scanner as an orchestrator over specialized open-source tools, not a bespoke static analysis engine. Each tool below has been refined to outperform a general scanner within its niche; the skill's contribution is the unified data model (Fideslang), the agentic reasoning over standardized outputs, and the multi-framework finding format.
## The Fideslang taxonomy
The Fideslang privacy taxonomy (developed by Ethyca, maintained with the IAB Tech Lab) is the standardization layer. Without it, each tool emits findings in its own ontology, and cross-tool reasoning is impossible.
Three orthogonal label axes:
* **Data categories**: what the data is. Hierarchical. `user.contact.email`, `user.health.genetic`, `user.financial.account_number`, `user.behavior.browsing_history`, `system.operations.logs`.
* **Data uses**: why it is processed. `provide.service.operations`, `improve.system.analytics`, `marketing.advertising.first_party.contextual`, `marketing.advertising.third_party.targeted`.
* **Data subjects**: who it concerns. `customer`, `employee`, `prospect`, `patient`, `child`.
Developers annotate code, OpenAPI specs, and YAML configurations with Fideslang labels. The agent's reasoning then operates against these labels, which are tool-agnostic.
### Hard rules over Fideslang label combinations
Some label combinations are categorical violations regardless of consent state, because they violate Article 9 prohibition (no Article 9(2) basis is configurable for them).
| Data category | Data use | Verdict |
|---------------|----------|---------|
| `user.health.*` | `marketing.advertising.*` | Article 9 violation, immediate |
| `user.biometric.*` | `marketing.advertising.*` | Article 9 violation, immediate |
| `user.genetic.*` | any except documented Art. 9(2) basis | Article 9 violation |
| `user.behavior.children.*` | `marketing.advertising.third_party.targeted` | Art. 8 (children's consent) + UK ICO Children's Code violation |
| `user.financial.account_number` (full PAN) | any logged or persisted state outside PCI scope | Art. 32 violation, plus PCI DSS scope violation |
The agent should encode these as deterministic rules running against the union of Fideslang annotations across the codebase.
## Privado - data flow mapping and RoPA automation
**Function**: identifies 110+ specific personal data elements in source code and traces their trajectory to sinks (databases, third-party APIs, log destinations).
**Why**: the only credible open-source tool for generating an actual data flow graph from source. The graph is the substrate against which RoPA drift is detected.
**Integration**:
```bash
privado scan <repo> --skip-dependency-check
```
Output: `.privado/privado.json` containing a knowledge graph of sources, processors, and sinks, each annotated with Fideslang-compatible labels.
**Privacy property**: Privado generates the graph natively, without sending source code to a cloud service. This is critical when the scanner itself is processing customer source code.
**Skill consumption**: the agent loads `privado.json`, extracts the flow set, and reconciles against `.compliance/ropa.yaml`. The reconciliation is the Tier 2 RoPA drift check.
## Semgrep - deterministic AST rule enforcement
**Function**: AST pattern matching at the speed required for synchronous PR-time checks.
**Why**: rules are expressed in a near-source-code DSL that engineers can read and write. Custom rules for organization-specific patterns (e.g., "no logging of `personnummer`") are cheap to author.
**Integration**: maintain a `.semgrep/gdpr/` rule directory. Run via:
```bash
semgrep --config .semgrep/gdpr --json .
```
**Skill consumption**: parse the JSON output, map each rule ID through the cross-framework table, emit unified findings.
**Privacy note**: Semgrep offers zero data retention for AI subprocessors and isolates customer data. This is relevant when the scanner is operated as a SaaS over customer code.
## Bearer - privacy-as-code auditing and reporting
**Function**: compiles privacy information required by legal teams; produces RoPA-shaped artifacts and risk reports.
**Why**: where Privado focuses on flow graph generation, Bearer focuses on continuous evidence artifact generation aligned to Article 30. Useful for the legal-engineering interface.
**Integration**:
```bash
bearer scan . --report privacy --format json
```
**Skill consumption**: Bearer's report becomes a secondary substrate for cross-validation against the Privado flow graph. Discrepancies between the two tools' findings are themselves signal (each tool has different blind spots).
## Helsinki GDPR Scanner - frontend consent and cookie validation
**Function**: scans rendered frontend pages against documented cookie banner site settings; detects scripts that fire before consent.
**Why**: Tier 1 deterministic checks on backend code do not catch the most common consent violation, which is frontend tracking SDKs that initialize on page load regardless of consent state.
**Integration** (Docker via Stonehenge):
```bash
docker run --rm -v $(pwd):/work helsinki-gdpr-scanner /work --output /work/json
```
**Skill consumption**: parses the JSON reports under `/json`, identifies tracking scripts that loaded prior to consent, and cross-references against `.compliance/consent_mappings.yaml` to determine whether each script's category required consent.
## Combining outputs: the unified finding format
Each tool emits in its own format. The agent normalizes to:
```json
{
"rule_id": "string",
"tool": "privado|semgrep|bearer|helsinki|builtin",
"severity": "critical|high|medium|low",
"tier": 1 | 2 | 3,
"file": "path",
"line": int,
"evidence": "string",
"fideslang": {
"data_categories": ["..."],
"data_uses": ["..."],
"data_subjects": ["..."]
},
"framework_mappings": {
"gdpr": ["Art. ..."],
"nist_800_53_r5": ["..."],
"iso_27001_2022": ["..."],
"soc2_tsc": ["..."],
"cis_v8": ["..."]
},
"remediation": "string",
"suppression_eligible": bool
}
```
A single semantic violation may surface in multiple tools' outputs. Deduplication key: `(file, line, fideslang.data_categories, framework_mappings.gdpr)`. When duplicates merge, retain the union of evidence statements and the highest severity.
## Execution ordering
For pull request gating, run in this order; later stages depend on earlier outputs:
1. **Secret scanners** (GitLeaks, Trufflehog) - fast, fail fast.
2. **Semgrep** - AST rules, parallelizable, completes in seconds.
3. **SCA / dependency scan** (Trivy, Snyk, Dependabot data) - parallelizable with Semgrep.
4. **Privado** - slower, requires graph construction. Begin in parallel with stages 2-3.
5. **Bearer** - parallel with Privado.
6. **Helsinki GDPR Scanner** - requires built frontend assets; runs after build stage.
7. **Agentic Tier 2 reasoning** - runs only after all Tier 1 outputs are normalized. Has the longest latency budget; should be skipped on PRs that don't trigger it (see SKILL.md trigger heuristics).
For nightly or scheduled deep scans, run all stages without parallelism budget concerns and run additional out-of-band checks (DAST, penetration smoke tests, transfer geolocation refresh).
@@ -0,0 +1,108 @@
# Schrems II and International Transfers
CJEU C-311/18 (Schrems II, 2020) invalidated the EU-US Privacy Shield and held that transfers under Standard Contractual Clauses (SCCs) require a case-by-case assessment of the destination country's legal framework. EDPB Recommendations 01/2020 establish the operational methodology: identify the transfer, identify the transfer tool (SCC, BCR, derogation), assess the destination law, identify supplementary measures, and re-evaluate periodically.
The 2021 modular SCCs (Commission Implementing Decision 2021/914) replaced the 2010 SCCs and structure the clauses by transfer scenario:
* Module 1: controller to controller
* Module 2: controller to processor
* Module 3: processor to processor
* Module 4: processor to controller
The scanner's job is to verify that for every detected restricted transfer, a SCC of the correct module exists, a Transfer Impact Assessment (TIA) is on file, and the technical supplementary measures the TIA promises are actually enforced in code.
## Detection: mapping data sinks to geography
Privado identifies external sinks in the data flow graph, including their hostnames and IP addresses. The agent enriches each sink with geographic metadata via:
* Reverse DNS to identify the hosting provider
* IP geolocation databases (MaxMind, IP2Location, RIPE)
* Provider-specific region tagging (AWS region, GCP region, Azure region encoded in the hostname or in the IaC resource)
* For SaaS endpoints, a maintained mapping of vendor data residency commitments (Stripe, Twilio, OpenAI, Anthropic, etc.)
A sink resolving outside the EEA, and not in a country covered by a current adequacy decision (Andorra, Argentina, Canada commercial, Faroe Islands, Guernsey, Israel, Isle of Man, Japan, Jersey, New Zealand, South Korea, Switzerland, United Kingdom, Uruguay, and the EU-US Data Privacy Framework for participating organizations as of the framework's status), is classified as a restricted transfer subject to Chapter V.
**Caveat**: adequacy decisions evolve. The scanner pins a dated reference table and emits a soft warning when the table is older than 90 days, prompting an update.
## Verification: SCC presence and module correctness
For each restricted transfer, the agent expects:
* A reference in `.compliance/transfers/transfers_inventory.yaml` to an SCC artifact under `.compliance/transfers/scc_*`.
* The SCC module corresponds to the actual data flow direction:
* The controller's own service exporting to a vendor's processor → Module 2
* Two of the controller's services in different jurisdictions both classifying as controllers (joint controllership scenarios) → Module 1
* The controller's processor sub-engaging another processor → Module 3
* The SCC artifact metadata names the parties, the categories of data subjects, the categories of personal data, the purposes of processing, and the retention period - and these match the corresponding RoPA entry.
Module mismatch is a common drift mode: a vendor relationship begins as a vanilla SaaS (Module 2) and evolves into a joint controllership (Module 1) without the SCC being updated.
## Verification: TIA presence and supplementary measures
The TIA evaluates whether the destination country's legal framework offers protections "essentially equivalent" to GDPR. EDPB Recommendations 01/2020 note that for several major destinations (notably the United States in respect of Section 702 FISA and Executive Order 12333), the answer is no without supplementary measures.
The TIA enumerates the supplementary measures the controller relies on. These are typically:
* Encryption in transit (TLS 1.3 with verified ciphersuites)
* Encryption at rest with a Customer Managed Key held inside the EEA, where the foreign processor cannot access the plaintext key material
* Pseudonymization of direct identifiers prior to transfer
* Contractual measures (notification of access requests by foreign authorities, transparency reports)
* Organizational measures (legal challenge of access requests, data minimization at the export boundary)
The scanner verifies that the technical measures are deterministically implemented:
### CMK enforcement (Tier 1)
For any storage resource (S3, RDS, GCS, Azure Blob, etc.) provisioned in a non-EEA region where the TIA promises CMK with EU-resident key material:
* The IaC must reference a KMS key whose key store is in an EEA region
* For AWS, this typically means a multi-region key with the primary in eu-* regions, or an external key store
* For cross-account access, the key policy must explicitly grant the destination service `kms:Decrypt` only via a documented temporary mechanism, not blanket access
### Pseudonymization at the boundary (Tier 1)
If the TIA promises that direct identifiers are pseudonymized prior to transfer, the agent looks for a transformation function on the egress path. Common patterns:
* A reverse-proxy or API gateway that hashes / tokenizes specific fields before forwarding
* A schema mapping in a streaming pipeline that drops or transforms identifier columns
* A dedicated tokenization service whose key material does not leave the EEA
If none of these exists in the codebase but the TIA claims pseudonymization, the scanner flags TIA-vs-implementation drift.
### Encryption in transit (Tier 1)
TLS 1.2+ with strong ciphersuites for the transfer leg. AST scan of HTTP client configuration for the destination service.
## Article 49 derogations
Article 49 permits certain transfers without an Article 46 safeguard, but only in narrow circumstances: explicit consent, contract with the data subject, important reasons of public interest, legal claims, vital interests, public register data. Derogations are not a fallback for ordinary commercial transfers.
The agent flags any RoPA entry where the international transfer safeguard is `derogation_art_49_X` and the processing is recurring, large-scale, or commercial. The EDPB has been explicit that derogations cannot be used systematically.
## The agent's check sequence per detected transfer
1. Locate the transfer in `.compliance/transfers/transfers_inventory.yaml`. If absent, emit a finding (Article 30 + Article 44 violation: undocumented restricted transfer).
2. Verify the SCC artifact exists at the referenced path. If absent, emit a finding.
3. Verify the SCC module matches the actual data flow direction. If mismatched, emit a finding.
4. Verify the TIA artifact exists. If absent, emit a finding.
5. Read the TIA's "supplementary measures" section. For each named technical measure, run the corresponding deterministic check against IaC and code. For each missing implementation, emit a finding.
6. For SaaS vendors: check the vendor's most recent Transparency Report against the TIA's vendor risk assumptions. (Tier 3 evidence pointer.)
## A worked example
A Swedish controller routes user contact data (`user.contact.email`) to AWS US-East-1 for processing by an analytics vendor.
* Restricted transfer detected (US, no current Privacy Shield, vendor not on EU-US DPF).
* `transfers_inventory.yaml` shows safeguard `scc_module_2`, references `transfers/scc_aws_us_east_1.pdf`, references `transfers/tia_aws_us_east_1.md`.
* SCC artifact present, Module 2 correct (controller-to-processor).
* TIA promises: TLS 1.3, AES-256 at rest with CMK held in eu-north-1, no pseudonymization (direct identifiers are necessary for the analytics use case).
* IaC check: S3 bucket references KMS key whose primary is in eu-north-1 and replicated to us-east-1 with restrictive access policy. Pass.
* TLS check: HTTPS endpoint uses TLS 1.3. Pass.
* No findings. Transfer is documented and technically supported.
A subsequent pull request adds a new field `user.health.conditions` to the analytics export. The agent now flags:
* Article 9 violation (special category data being processed under SCC Module 2 with no explicit Article 9(2) basis declared)
* TIA-vs-implementation drift: the TIA's risk assessment did not contemplate health data; the supplementary measures are inadequate for special category data crossing into a jurisdiction with broad surveillance authorities
* Mandatory DPO escalation before merge.
This is exactly the kind of case where the scanner adds asymmetric value: the change looks innocuous to the engineer (one extra column) but materially alters the legal posture.
@@ -0,0 +1,113 @@
# Verification Tiers
The honest scope of automated GDPR detection. Every requirement maps to one of three tiers; misclassification is the primary cause of false confidence in privacy tooling. The ordering below is by tier, not by article number, because the operational decision (which engine handles this check?) is tier-driven.
## Tier 1: deterministically verifiable from the repository
Static analysis with high confidence. AST parsing, regex, schema validation, IaC parsing. The GDPR-Bench-Android study found that formal-AST baselines achieve high specificity but limited recall; LLMs improve line-level recall (Qwen2.5-72B at 61.6% Accuracy@1 line-level) at the cost of some specificity. For Tier 1 checks that are mechanical, prefer the deterministic engine.
### Hardcoded secrets and credentials (Art. 32)
API keys, OAuth tokens, database passwords, private keys committed to source. Direct, indisputable Article 32 violation. Run secret scanners across the full Git history, not just HEAD; secrets remain compromised after they are removed from the working tree.
Tools: GitLeaks, Semgrep secret rules, Trufflehog. Wrap as a pre-commit hook *and* a CI gate; the hook fails fast for the developer, the CI gate is the audit-grade signal.
### Insecure data transmission (Art. 32, Art. 5(1)(f))
Plain-HTTP API calls, deprecated TLS configurations (SSLv3, TLS 1.0, TLS 1.1), weak cipher suites, missing certificate validation (`verify=False` in requests, `rejectUnauthorized: false` in Node). AST-detectable.
### Missing cryptographic primitives on PII (Art. 32)
ORM column definitions storing entities matching known sensitive patterns (`SocialSecurityNumber`, `personnummer`, `HealthRecord`, anything Fideslang-labeled `user.payment.*`, `user.health.*`, `user.biometric.*`, `user.genetic.*`) without encryption decorators or hashing functions. AST-detectable when the ORM is annotation-driven (TypeORM, SQLAlchemy with declarative base, Hibernate JPA).
### Unconstrained data collection (Art. 5(1)(c) data minimization)
OpenAPI request payload schemas accepting fields not functionally required for the documented endpoint operation. The deterministic check is `additionalProperties: false`; without it, the schema admits unbounded extraneous PII.
### Invasive third-party dependencies (Art. 6, Art. 25)
Tracking SDKs, analytics libraries, advertising networks present in dependency manifests (`package.json`, `build.gradle`, `pom.xml`, `requirements.txt`, HTML script tags) without consent management orchestration. The skill maintains a denylist of known-invasive SDKs; presence triggers a Tier 2 follow-up to validate consent gating.
### Soft-delete masquerading as Article 17 erasure
AST pattern matching on controller methods registered as the erasure endpoint. If the body is `UPDATE ... SET is_deleted = ...` or equivalent ORM `update()` rather than `DELETE` (or a documented cryptographic shredding call), it is non-compliant.
### IaC default-secure violations (Art. 25)
Terraform / CloudFormation / Pulumi / Kubernetes manifest parsing for:
* Storage resources without encryption configuration
* Storage resources with public ACLs
* Network rules with `0.0.0.0/0` ingress on ports serving PII workloads
* IAM policies with wildcard actions on resources holding PII
* Missing mandatory security headers on API gateway / ingress configurations
Tools: Checkov, Trivy IaC, OPA/Rego, tfsec, Kics. The agent should run multiple in parallel and deduplicate findings by `(file, line, semantic_rule)`.
## Tier 2: agentic reasoning over policy artifacts
Requires natural-language understanding cross-referenced with code reality. An LLM (or a ReAct-style agent; the GDPR-Bench-Android study reported the highest file-level Accuracy@1 of 17.4% from a ReAct configuration) extracts prescriptive statements from policy markdown, then validates implementation against them. The prompt templates for these checks are in `agentic-prompts.md`.
### RoPA drift (Art. 30)
The agent receives the parsed `.compliance/ropa.yaml` and the Privado data flow JSON. It enumerates flows present in the code but not in the RoPA (under-documentation, the Article 30 violation) and flows present in the RoPA but not in the code (stale documentation, a less severe but still flagged drift).
### Purpose limitation (Art. 5(1)(b), Art. 6)
The agent reads the OpenAPI spec for an endpoint, extracts the documented business purpose, then reads the controller method to enumerate which data objects are queried and returned. If the data objects exceed what the documented purpose justifies, the agent flags purpose creep.
### Consent logic vs. cookie policy (Art. 6, Art. 7)
The agent reads `.compliance/consent_mappings.yaml` and the frontend consent management code. It validates that scripts categorized as analytics or marketing only initialize after the corresponding consent state is recorded. The check requires understanding both the policy intent and the runtime control flow.
### DPIA-vs-implementation drift (Art. 35)
The agent reads each DPIA in `.compliance/dpia_inventory/`, extracts the "measures envisaged" section, and verifies each named control exists in code. A DPIA promising "all access logged to immutable storage" must have a corresponding logging configuration in IaC.
### Privacy policy semantic alignment (Art. 12, Art. 13)
The agent compares `.compliance/privacy_policy.md` claims about data categories collected, processing purposes, recipients, and retention against the union of all detected processing in code. Mismatches are misrepresentations to data subjects, an Article 12/13 violation.
### Incident response runbook completeness (Art. 33, Art. 34)
The agent reads `.compliance/incident_response.md` and validates it against the EDPB Guidelines 9/2022 phase structure (detection, assessment, containment, escalation, notification, communication). Missing phases or deprecated communication channels block the build.
## Tier 3: fundamentally out of repo
The repository cannot prove these. The scanner can at most check for an *evidence pointer*: a URI, signed credential, or external API reference that points to where the proof lives.
### Physical security (related to Art. 32 organizational measures)
A data center server room being locked is unprovable from code. The scanner expects `.compliance/evidence_pointers.yaml` to contain a URI to the latest physical facility audit (SOC 2 report covering the colocation provider, ISO 27001 certificate scope, etc.).
### Employee privacy training (Art. 39 DPO support, organizational measures)
The scanner expects an evidence pointer to an HR / LMS API endpoint confirming all currently active contributors (matched by email against the Git committer set) have completed annual privacy training within a documented validity window.
### Executed DPA / SCC contracts (Art. 28, Art. 46)
The repository may contain Markdown templates of Data Processing Agreements and SCCs. The scanner cannot verify that a specific vendor *signed* a specific instance. The evidence pointer is an URI to the contract management system (DocuSign API, Ironclad, etc.) returning the signed instance keyed by vendor + module + execution date.
### Regulator notification logs (Art. 33)
Whether a notification was actually sent to the supervisory authority within 72 hours of awareness is a runtime, organizational fact. The scanner expects an evidence pointer to a notification log API.
### Board independence and governance (organizational measures)
Governance structure, role separation between DPO and CISO, board oversight of the privacy program. Out of repo entirely. The scanner verifies that the *policy* documents these structures exist, not that they actually do.
## The Honest Output Pattern
When the scanner emits a finding, it should annotate the tier explicitly:
```
[GDPR Art. 32 / SC-28 / A.8.24]
Tier 1 (deterministic): server_side_encryption_configuration missing on aws_s3_bucket.user_uploads.
[GDPR Art. 30]
Tier 2 (agentic): ropa.yaml does not document the data flow from /api/v1/orders to Stripe (detected by Privado at services/checkout.ts:142).
[GDPR Art. 39 / Organizational]
Tier 3 (out of repo): no evidence pointer found for employee privacy training. Add an entry to .compliance/evidence_pointers.yaml referencing the LMS API.
```
The user knows immediately which findings can be fixed by editing code, which require updating policy markdown, and which require organizational action outside engineering's scope. This honesty is what makes the scanner trustworthy.
@@ -0,0 +1,126 @@
# Violation Patterns
Real-world heuristics for the scanner. Each pattern below is drawn either from a regulatory enforcement action, an architectural failure mode documented in the GDPR-Bench-Android study, or a publicly disclosed supply chain incident. The agent should treat these as concrete detection targets rather than abstract guidelines.
## Code-level anti-patterns
### Sensitive API access without consent gate (Art. 6)
**Pattern**: invocation of camera, microphone, geolocation, contacts, or biometric APIs without a preceding conditional checking the documented consent state.
**Android example** (from the source document):
```java
manager.openCamera(cameraId, stateCallback, null); // no consent check
```
**Detection**: AST scan for the sensitive API surface (per platform: `android.hardware.camera2`, `CoreLocation`, `navigator.geolocation`, `MediaDevices.getUserMedia`), then walk up the control flow graph looking for a guard against a consent provider. Absence of the guard is the finding.
**Why it lands as Article 6**: any processing operation needs a lawful basis. For sensor APIs in a consumer context, the basis is almost always consent (Art. 6(1)(a)). Capturing without checking the consent state means processing without lawful basis.
### Unsafe logging of PII (Art. 32, Art. 5(1)(f))
**Pattern**: stack traces, request bodies, or full user objects written to standard output, log files, or third-party log aggregators.
**Examples**:
```javascript
console.log(userObject); // Node.js
logger.info("Failed login for " + email); // Java/Python
fmt.Printf("%+v\n", request) // Go, with PII in request
```
**Detection**: Semgrep rules for `console.log`, `logger.*`, `print` calls whose arguments resolve to variables flagged by Privado as containing PII.
**Operational nuance**: distinguishing safe logging (log a request ID, not a request body) from unsafe logging is exactly the kind of check where deterministic AST analysis underperforms LLM analysis. The skill should run a Semgrep first pass for the obviously bad patterns, then escalate ambiguous cases to the agentic auditor.
### Inadequate Article 17 erasure
**Pattern**: the endpoint registered to satisfy the right to erasure performs a soft-delete (boolean flag, status enum change, timestamp on a `deleted_at` column) rather than hard deletion or cryptographic shredding.
**Detection**: identify the erasure endpoint via:
* OpenAPI tag `dsar` or `privacy`
* Convention paths (`/api/.*privacy.*delete`, `/api/.*forget`)
* DSAR runbook reference
Then trace the controller method statically. Any path that reaches a SQL `UPDATE` (or ORM `update()`) instead of `DELETE` (or a documented anonymization function) is the finding.
**Edge case**: legitimate retention obligations (tax law, anti-money-laundering) may legally override the right to erasure for specific fields. The DPIA or RoPA should document this; the scanner should escalate to the agentic auditor when it detects soft-delete patterns to check for a documented retention basis rather than auto-flagging.
### Plaintext PII in error responses (Art. 32)
**Pattern**: API error handlers reflecting PII back to the caller in error messages or stack traces.
```python
@app.errorhandler(500)
def handle_500(e):
return {"error": str(e), "context": request.json}, 500 # leaks PII
```
**Detection**: Semgrep on error handler bodies that reference request bodies, query parameters, or session objects.
## Configuration-level anti-patterns
### Overly permissive CORS (Art. 25, Art. 32)
**Pattern**: `Access-Control-Allow-Origin: *` on endpoints serving authenticated PII; `Access-Control-Allow-Credentials: true` paired with permissive origins.
**Detection**: parse middleware configuration (Express, FastAPI, Spring), API gateway resource policies, and CDN configurations. Cross-reference with the endpoint inventory; permissive CORS on a public marketing endpoint is fine, on `/api/v1/users/me` it is not.
### Mobile permission overreach (Art. 5(1)(c))
**Pattern**: `AndroidManifest.xml` requesting `ACCESS_FINE_LOCATION`, `READ_CONTACTS`, `READ_SMS`, `RECORD_AUDIO`, `CAMERA` when the app's documented purposes do not require them. iOS equivalent: `Info.plist` `NS*UsageDescription` keys.
**Detection**: enumerate requested permissions, cross-reference each against the Fideslang `data_categories` documented in `.compliance/ropa.yaml`. An app that lists no `user.location` data category in the RoPA but requests `ACCESS_FINE_LOCATION` is flagged.
### Disabled or missing security headers (Art. 32)
**Pattern**: web server configurations missing `Strict-Transport-Security`, `Content-Security-Policy`, `X-Content-Type-Options: nosniff`, `X-Frame-Options`, or `Referrer-Policy`.
**Detection**: parse Nginx, Apache, Caddy, Cloudflare, AWS CloudFront, or framework-level header configurations. Check for the canonical set; emit findings per missing header.
## Dependency and supply chain risks
### Invasive third-party SDKs
**Pattern**: presence of advertising or analytics SDKs known for covert tracking or device fingerprinting without valid consent.
**Regulatory anchors**:
* CNIL fined Apple and Voodoo Games in 2023 over advertising identifier use without adequate consent
* CNIL fined Clearview AI repeatedly for non-compliant biometric scraping
* IMY (Sweden) has issued multiple decisions on Google Analytics post-Schrems II
**Detection**: maintain a denylist of SDK package coordinates (e.g., `com.facebook.android:facebook-android-sdk` for advertising-grade fingerprinting, specific MMP SDKs, ad-tech identifiers). Match against `package.json`, `build.gradle`, `Podfile`, etc. Presence requires Tier 2 escalation to verify consent gating.
### Compromised OAuth integrations
**Pattern**: outdated authentication libraries or OAuth client implementations vulnerable to known account takeover patterns.
**Reference incident**: the SalesLoft / Drift supply chain compromise demonstrated that OAuth tokens leaked through an integrated tool propagated to dozens of downstream Salesforce instances, each constituting an Article 32 confidentiality breach.
**Detection**: SCA tools (Snyk, Dependabot, Trivy, Grype) cross-referenced with NVD CVE data for OAuth, SAML, and session-management libraries. Combine with secret scanning for committed OAuth refresh tokens and client secrets.
### Transitive dependency backdoors
**Pattern**: deeply nested dependencies containing data-harvesting code that exfiltrates environment variables, secrets, or PII to external servers.
**Reference incident**: the Shai-Hulud npm backdoor pattern, where compromised packages walked the environment looking for credentials and posted them to attacker-controlled hosts.
**Detection**: SCA combined with malware scanners (Phylum, Socket, Snyk Malicious Packages). The scanner should inspect not just direct dependencies but the full transitive closure, with particular attention to recently published versions of long-lived packages and to packages with low download counts being added near production paths.
### Customer management system exposure
**Pattern**: customer-facing applications with exposed administrative interfaces, weak authentication on internal CRMs, or insecure data export endpoints.
**Reference incident**: the Italian Garante fined Enel substantially for failing to secure a customer management system that enabled unauthorized data acquisition by third parties.
**Detection**: out-of-band, requires DAST (dynamic application security testing). The static scanner can verify that admin routes have RBAC middleware applied; it cannot verify that the RBAC is correctly enforced at runtime without DAST.
## Combining patterns into severity
A single low-severity finding rarely justifies blocking a deployment. Combinations do:
* **Critical (block)**: hardcoded production credentials + plaintext PII transmission. The combined exposure is multiplicative.
* **Critical (block)**: special category data (Art. 9) + missing encryption at rest + non-EEA data sink. Three-way violation: Art. 9(2) basis missing, Art. 32 inadequate, Art. 44 transfer inadequate.
* **High (warn)**: invasive SDK present + no consent gating + frontend tracker initializes pre-consent. Defer to agentic Tier 2 to confirm before blocking.
* **Medium (warn)**: missing security header + cookie tagged as analytics without consent flow.
The agent should compute these combinations rather than emitting each finding in isolation. The blast radius of compounded violations is what regulators actually fine on.
@@ -0,0 +1,165 @@
---
name: iso-27001-2022-compliance
description: Authoritative reference for verifying ISO/IEC 27001:2022 compliance from a code repository. Use whenever a task involves auditing, automating, or generating evidence for ISO 27001 controls, the ISMS document set (Clauses 4-10), the Statement of Applicability (SoA), or Annex A controls (A.5 Organizational, A.6 People, A.7 Physical, A.8 Technological). Trigger on mentions of ISO 27001, ISO/IEC 27001, 27001:2022, ISMS, SoA, Annex A, "Statement of Applicability", a specific control identifier (A.5.x, A.6.x, A.7.x, A.8.x), Checkov/Trivy/Steampipe ISO compliance specs, the 2013-to-2022 control transition, NIST 800-53 / CIS Controls / SOC 2 crosswalks, agentic auditing of policy documents, or building CI/CD pipeline gates for security compliance. Trigger even when phrasing is indirect — "is our Terraform compliant", "scan our policies for audit readiness", "what does the standard require for cryptography", "map our controls to SOC 2", "build a compliance scanner" — these all qualify.
---
# ISO/IEC 27001:2022 Compliance Verification Skill
## Purpose
This skill provides authoritative, repository-grounded reference data for two distinct verification modes against ISO/IEC 27001:2022:
1. **Deterministic CI/CD scanning** — Pull-Request-time evaluation of code, IaC, and dependency manifests against Annex A.8 (Technological Controls).
2. **Agentic auditing** — On-demand semantic review of ISMS policy documents (Clauses 4-10) and organizational controls (Annex A.5).
The skill is designed for engineers building compliance automation, not for end-user training. It is a compliance oracle: when in doubt about what the standard actually requires, consult this skill rather than relying on memory.
## Why two modes exist
ISO 27001:2022 is structurally bifurcated. Clauses 4-10 govern the management system (governance, scope, leadership, planning, operation, performance, improvement) and are expressed in natural-language documents. Annex A enumerates 93 security controls, the majority of which (the 34 in A.8) map directly to repository artifacts. Deterministic parsers handle A.8; LLM-based agentic auditors handle Clauses 4-10 and A.5. A.7 (Physical) and most of A.6 (People) sit out-of-repo and require evidence pointers, not direct verification.
## Routing — read this first
Before consulting any reference file, identify which mode the task requires:
- Task touches Terraform, CloudFormation, Kubernetes manifests, Dockerfiles, application source code, package manifests, CI/CD definitions, or branch protection → **deterministic mode**. Read `references/annex-a-controls.md` and `references/violation-patterns.md`.
- Task touches Markdown policies, the Information Security Policy, Risk Register, SoA, Management Review minutes, internal audit logs → **agentic mode**. Read `references/isms-clauses.md` and `references/agentic-prompts.md`.
- Task asks about new controls introduced in the 2022 revision → read `references/new-controls-2022.md`.
- Task references a 2013-era control identifier (e.g., A.14.2.1, A.12.1.2) → read `references/legacy-mapping.md` to translate before responding.
- Task involves NIST 800-53, CIS Controls v8.1, or SOC 2 → read `references/cross-framework-mapping.md`.
- Task involves wrapping Checkov, Trivy, or Steampipe → read `references/tool-orchestration.md`.
## The Statement of Applicability is the routing table
Never apply all 93 Annex A controls blindly. Compliance is governed by the organization's SoA (Clause 6.1.3d), which declares which controls are included, which are excluded, and the justification for each. A machine-readable SoA (`soa.json` or `soa.csv`, conventionally located at `.security/` or `ISMS/`) is the primary input to any compliance scan. If the SoA is missing, the first finding is always: "Cannot determine control applicability — Clause 6.1.3 violation."
A valid SoA contains four core elements per control:
1. The control identifier and title.
2. A statement of inclusion or exclusion.
3. Justification (operational reasoning for inclusion, risk-based reasoning for exclusion).
4. Implementation status (Implemented, Planned, Not Applicable).
Suppression logic for the scanner:
- Excluded controls → suppress all checks. Do not generate findings.
- Included + Implemented → run all checks. Failures are blocking.
- Included + Planned → run all checks. Failures are warnings, not blocking.
- Included + Not Applicable → structural contradiction. Flag as SoA inconsistency.
## Three verification typologies
Every Annex A control falls into one of three categories. Be explicit about which one applies before claiming a control has been verified.
### Deterministic in-repo
Structured parsing produces a binary outcome. Examples: A.8.24 (encryption flags in IaC), A.8.4 (branch protection rules), A.8.8 (CVEs in dependency manifests), A.8.28 (SAST presence in pipelines). These are the highest-leverage targets for automation.
### Agentic over policy artifacts
Constrained LLM evaluation of natural-language documents. Examples: Clause 5.2 (Information Security Policy adequacy), Clause 9.3 (Management Review completeness), A.5.1 (Policies for information security), A.5.7 (Threat Intelligence — does the documented threat-intel process actually address the control intent?). The LLM produces "Observations" and "Potential Non-Conformities" for human validation. Never autonomous decisions.
### Out-of-repo / evidence pointer
Cannot be verified from the repository alone. Examples: A.7.x (all Physical Controls), A.6.1 (Screening), A.6.4 (Disciplinary process). The skill's job is to confirm that the policy exists AND that the policy contains a pointer (URL, API reference, named system of record) to the external evidence source. If both the policy and the pointer are absent, flag a documentation gap. Never claim the underlying control is "verified."
## Trigger mechanics for CI/CD integration
The scanner must select sub-modules based on the Git diff to avoid running the full audit on trivial commits:
| Files changed | Modules to run |
|---|---|
| `*.tf`, `*.yml`, `*.yaml`, `*.json`, `Dockerfile`, K8s manifests | Checkov (IaC) → A.8.9, A.8.20, A.8.22, A.8.24, A.8.27 |
| `package-lock.json`, `requirements.txt`, `go.sum`, `Cargo.lock`, `pom.xml` | Trivy (SCA) → A.8.8, A.8.30 |
| Application source code | SAST + secret scanning → A.8.24, A.8.28 |
| `.github/workflows/*`, `.gitlab-ci.yml` | Pipeline integrity → A.8.25, A.8.29, A.8.31, A.8.32 |
| `ISMS/**/*.md`, `ISMS/**/*.docx`, `*.md` policies | Agentic LLM auditor → Clauses 4-10, A.5.x |
| `soa.json`, `soa.csv` | SoA structural validation → Clause 6.1.3 |
## Suppression and risk acceptance — the critical coupling
Inline suppressions in scanner output (e.g., `# checkov:skip=CKV_AWS_19`) are legitimate only when paired with a documented, executive-approved Risk Register entry. The scanner must enforce this coupling:
1. Parse suppression comments for a Risk ID reference.
2. Cross-reference the Risk ID against the canonical Risk Register (Clause 8.2 artifact, typically `Risk_Register.xlsx` or equivalent).
3. Verify the entry exists, has a Risk Owner, has a residual risk score, and is referenced in the Risk Treatment Plan (Clause 8.3).
4. If any of these are missing, the suppression is invalid. Generate a Clause 6.1.3 finding regardless of the technical check passing.
This mechanism is the binding contract between developer behavior and GRC oversight. Without it, suppressions become a compliance laundering mechanism.
## Honest limitations — surface these proactively
Automated scanners cannot prove physical reality, behavioral compliance, or runtime state. When generating reports, label every finding with its verification scope:
- "Documentation Verified — Physical Validation Required" for A.7.x policies.
- "Policy Verified — Behavioral Compliance Out of Scope" for A.6.3 (training), A.7.7 (clear desk).
- "Point-in-Time" for any deterministic check. Continuous runtime monitoring (A.8.16) is required to detect drift after merge.
For agentic AI workflows in the audited system itself, reference the OWASP Top 10 for Agentic Applications and demand heightened governance controls. Standard SOC 2 / ISO 27001 controls assume deterministic systems and are insufficient for autonomous agents.
Read `references/limitations.md` for the full catalog of what automation cannot verify and how to communicate this to auditors.
## Required ISMS directory structure
Expect (and enforce) this canonical layout:
```
ISMS/
├── Governance/
│ ├── ISMS_Scope.md (Clause 4.3)
│ ├── InfoSec_Policy.md (Clause 5.2)
│ └── Security_Objectives.md (Clause 6.2)
├── Risk/
│ ├── Risk_Methodology.md (Clause 6.1.2)
│ ├── Risk_Register.xlsx (Clause 8.2)
│ └── Risk_Treatment.md (Clause 8.3)
├── SoA/
│ └── SoA.json (Clause 6.1.3)
├── Competence/
│ └── Competency_Matrix.csv (Clause 7.2)
└── Audits/
├── Internal_Audit_Log.md (Clause 9.2)
└── Mgmt_Review_Minutes.md (Clause 9.3)
```
For each document, beyond existence, verify: classification mark-up (Confidential / Internal Use Only), version control table, executive sign-off, review timestamp within the declared annual interval. Missing metadata is a Clause 7.5 (Documented Information) violation.
## Output report structure
Every compliance scan output should follow this template so findings are auditor-defensible:
```
# ISO/IEC 27001:2022 Compliance Report
## Repository: <name> | Commit: <sha> | SoA Version: <version> | Date: <ISO>
## Summary
- Controls in scope (per SoA): <N>
- Findings: <critical> critical, <high> high, <medium> medium, <low> low
- Documentation gaps: <N>
- Suppression integrity: <pass/fail>
## Findings by Annex A theme
### Organizational Controls (A.5)
### People Controls (A.6)
### Physical Controls (A.7)
### Technological Controls (A.8)
## ISMS Clause Findings (4-10)
## Cross-framework mapping (if requested)
- NIST SP 800-53 Rev. 5
- CIS Controls v8.1
- SOC 2 TSC
## Verification scope disclaimer
- Point-in-time scan of commit <sha>
- Out-of-repo controls verified by evidence pointer only
- Behavioral compliance not in scope
```
## Reference files
- `references/annex-a-controls.md` — Full catalog of Annex A.5, A.6, A.7, A.8 with verification logic per control.
- `references/new-controls-2022.md` — The 11 net-new controls in the 2022 revision and their automation implications.
- `references/legacy-mapping.md` — 2013 → 2022 control identifier translation matrix.
- `references/isms-clauses.md` — Clauses 4-10 mandatory documents and content expectations.
- `references/violation-patterns.md` — Real-world code, configuration, and dependency violation patterns with detection logic.
- `references/cross-framework-mapping.md` — NIST 800-53 Rev. 5, CIS Controls v8.1, SOC 2 TSC crosswalks.
- `references/tool-orchestration.md` — Checkov / Trivy / Steampipe wrapping patterns, including custom YAML policies.
- `references/agentic-prompts.md` — Constrained LLM prompts for policy and management review evaluation.
- `references/limitations.md` — Catalog of what automation cannot verify and required disclaimers.
@@ -0,0 +1,286 @@
# Agentic Prompts — Constrained LLM Evaluation of Policy Artifacts
The deterministic scanner cannot evaluate the semantic adequacy of a policy document. For Clauses 4-10 and most A.5 controls, the skill must use a constrained LLM auditor.
These prompts are designed to:
- Force the LLM into auditor role with explicit normative criteria.
- Suppress hallucination by demanding citation of clause identifiers.
- Output structured findings (Non-Conformity, Observation, Pass) that can be aggregated into reports.
- Refuse autonomous certification — every output is "for human validation".
The LLM operates in advisory mode only. It never closes findings, never approves documents, never overrides human reviewers.
---
## System prompt — base auditor persona
Use this as the system message for every agentic auditor invocation:
```
You are a certified ISO/IEC 27001:2022 Lead Auditor performing a documentation review.
Your role:
- Evaluate documents strictly against the normative requirements of ISO/IEC 27001:2022.
- Cite the specific Clause or Annex A control identifier for every finding.
- Output findings in three categories: Non-Conformity (NC), Observation (OBS), Pass.
- A Non-Conformity is a clear deviation from a normative requirement. An Observation is a weakness or improvement opportunity that does not (yet) breach a requirement.
- Never make assumptions about content not present in the document. If a requirement is not addressed, the finding is "Requirement not addressed" — do not infer intent.
- Flag generic, AI-generated, or boilerplate language as an Observation.
- Output is advisory only. Human auditors make all final determinations.
Output format (JSON):
{
"document_evaluated": "<filename>",
"clauses_in_scope": ["<clause id>", ...],
"findings": [
{
"type": "NC" | "OBS" | "Pass",
"clause_or_control": "<id>",
"requirement": "<exact normative requirement text>",
"evidence": "<quoted text from document or 'absent'>",
"rationale": "<one-sentence explanation>"
}
],
"boilerplate_risk": "low" | "medium" | "high",
"human_review_required": true
}
```
---
## Information Security Policy review (Clause 5.2 + A.5.1)
User prompt:
```
Document under review: <filename>
Document content:
<<<
{document_text}
>>>
Evaluate this Information Security Policy against ISO/IEC 27001:2022 Clause 5.2 and Annex A control A.5.1.
Required elements:
1. Establishes information security objectives or provides a framework for setting them (Clause 5.2.b).
2. Includes a commitment to satisfy applicable requirements related to information security (Clause 5.2.c).
3. Includes a commitment to continual improvement of the ISMS (Clause 5.2.d).
4. Is approved by top management (evidence of executive sign-off).
5. Is communicated within the organization (reference to distribution mechanism).
6. Is available to interested parties as appropriate (reference to availability).
7. Is reviewed at planned intervals (reference to review cadence).
For each required element, output a finding (Pass / OBS / NC) with the exact quoted evidence or "absent".
```
---
## Statement of Applicability review (Clause 6.1.3)
```
Document under review: <filename>
Format: JSON / CSV
Content:
<<<
{soa_content}
>>>
Evaluate against ISO/IEC 27001:2022 Clause 6.1.3(d).
Structural requirements:
1. Lists all 93 controls of Annex A:2022 (or equivalent justification for omitted controls).
2. For each control, declares status: Included or Excluded.
3. For Included controls, provides operational justification.
4. For Excluded controls, provides risk-based justification.
5. For Included controls, declares implementation status: Implemented, Planned, or Not Applicable (with rationale).
Cross-checks:
- Are any of the 11 net-new 2022 controls missing entirely? (A.5.7, A.5.23, A.5.30, A.7.4, A.8.9, A.8.10, A.8.11, A.8.12, A.8.16, A.8.23, A.8.28)
- Are exclusion justifications generic ("not applicable") or specific (referencing organizational context)?
- Do "Implemented" claims have plausible scope given the organization size and industry?
Output structural findings AND a list of any 2013-era control identifiers detected (these indicate incomplete transition).
```
---
## Risk Register review (Clause 8.2)
```
Document under review: <filename>
Format: spreadsheet / structured data
Content:
<<<
{risk_register_content}
>>>
Evaluate against ISO/IEC 27001:2022 Clause 8.2 and Clause 6.1.2.
Per-entry requirements:
1. Unique Risk ID.
2. Risk description (threat × vulnerability × asset).
3. Risk owner (named individual or role).
4. Inherent risk score (using documented methodology).
5. Treatment decision (Mitigate, Transfer, Accept, Avoid).
6. If Mitigate: linked control(s) from SoA.
7. Residual risk score after treatment.
8. Acceptance criteria reference (for Accept decisions, must reference Clause 8.3 risk acceptance authority).
Cross-cutting requirements:
- All accepted risks above threshold have executive sign-off.
- Linked controls in SoA are marked as Included.
- No orphan controls (controls in SoA marked Implemented but no Risk Register entry references them — possible compliance theater).
For each finding, output the Risk ID and the missing field.
```
---
## Management Review minutes (Clause 9.3)
```
Document under review: <filename>
Content:
<<<
{minutes_text}
>>>
Evaluate against ISO/IEC 27001:2022 Clause 9.3.2 (Management review inputs).
The minutes MUST cover all seven mandatory inputs:
a) Status of actions from previous management reviews.
b) Changes in external and internal issues relevant to the ISMS.
c) Changes in needs and expectations of interested parties.
d) Feedback on information security performance, including:
- Nonconformities and corrective actions
- Monitoring and measurement results
- Audit results
- Fulfilment of information security objectives
e) Feedback from interested parties.
f) Results of risk assessment and status of risk treatment plan.
g) Opportunities for continual improvement.
The minutes MUST also cover (Clause 9.3.3):
- Decisions related to continual improvement opportunities.
- Decisions for any need for changes to the ISMS.
Per-input output:
{
"input_id": "9.3.2.a",
"label": "Status of previous actions",
"covered": true | false,
"evidence": "<quoted excerpt or 'absent'>",
"depth": "substantive" | "perfunctory" | "absent"
}
Also evaluate: Is there evidence of executive participation (named C-suite attendees)? Are decisions documented with action owners and timelines?
```
---
## Risk Treatment Plan review (Clause 8.3)
```
Document under review: <filename>
Content:
<<<
{rtp_text}
>>>
Evaluate against ISO/IEC 27001:2022 Clause 6.1.3 and Clause 8.3.
Required content:
1. Treatment options chosen for each risk in the Risk Register.
2. Controls determined to implement each chosen treatment.
3. Comparison of determined controls with Annex A (gap analysis).
4. Justification for any controls outside of Annex A.
5. Approval of the plan by risk owners.
6. Approval of acceptance of residual risks by risk owners.
Cross-references:
- Every Risk Register risk has a corresponding entry in the RTP.
- Every linked control is also in the SoA as Included.
- No "Implemented" controls in SoA without a corresponding RTP entry justifying their inclusion.
Output gaps where the RTP, Risk Register, and SoA are inconsistent.
```
---
## Internal Audit review (Clause 9.2)
```
Documents under review: <filenames>
Content:
<<<
{audit_log_text}
{audit_findings_text}
>>>
Evaluate against ISO/IEC 27001:2022 Clause 9.2.
Required elements:
1. Audit programme covering all parts of the ISMS at planned intervals (typically annually).
2. Audit criteria, scope, frequency, and methods defined.
3. Auditor independence (auditor is not auditing their own work).
4. Reported findings to relevant management.
5. Evidence of corrective actions for non-conformities.
For each audit cycle, output:
- Date(s) of audit.
- Auditor identity and independence statement.
- Scope (which clauses / which Annex A controls / which business units).
- Findings count by severity.
- Status of corrective actions (open / closed / overdue).
Flag if any audit was conducted by the same person who owns the audited area.
```
---
## Generic policy adequacy review
For organizational policies (A.5.x) where there is no specific clause prompt, use:
```
Document under review: <filename>
Mapped controls: <list of A.5.x controls this document is intended to satisfy>
Content:
<<<
{policy_text}
>>>
For each mapped control, evaluate:
1. Does the policy text address the control's purpose as defined in ISO/IEC 27002:2022 implementation guidance?
2. Are responsibilities assigned (named roles, not just "the team")?
3. Are review and update mechanisms defined?
4. Is the policy specific to this organization (vs generic boilerplate)?
5. Are there enforcement mechanisms (consequences for violation)?
Boilerplate detection: Search for phrases that suggest the document is a generic template:
- "the company" / "the organization" with no actual company name.
- Generic threat lists with no organizational risk context.
- Identical phrasing to common policy templates on GitHub.
Output one finding per mapped control plus a boilerplate_risk score.
```
---
## Constraints and safety
The agentic auditor must never:
1. Approve a document. Approval is a human management responsibility.
2. Mark a control as "Implemented" based solely on policy review. Implementation requires technical evidence beyond the policy itself.
3. Generate certification decisions. Certification is performed by accredited certification bodies.
4. Apply checks to controls excluded by the SoA, unless asked to evaluate the exclusion justification itself.
5. Hallucinate references. Every clause citation must be a real ISO 27001:2022 clause. If unsure, say "uncertain — human review".
Every agentic finding must carry `human_review_required: true`. The skill's report generator must surface this field prominently so that no automated CI/CD step interprets agentic findings as final.
## Token / cost considerations
For large policy bundles, batch by document type rather than concatenating everything. Each document gets its own auditor invocation with the prompt template above. This keeps prompts within reasonable token budgets and produces auditable per-document outputs.
For very large repositories, run agentic checks only on changed documents (Git diff filter on `*.md` in `ISMS/` directory). Full agentic re-scan should run on a scheduled cadence (weekly / monthly) outside of PR-time CI/CD.
@@ -0,0 +1,161 @@
# Annex A Controls — Full Catalog with Verification Logic
ISO/IEC 27001:2022 Annex A contains 93 controls across four themes. This file is the authoritative per-control reference. Use it to look up exact control intent and the deterministic / agentic / out-of-repo verification logic for each.
## Theme structure
| Theme | Range | Count | Primary mode |
|---|---|---|---|
| A.5 Organizational | A.5.1 – A.5.37 | 37 | Agentic |
| A.6 People | A.6.1 – A.6.8 | 8 | Out-of-repo (evidence pointers) |
| A.7 Physical | A.7.1 – A.7.14 | 14 | Out-of-repo (evidence pointers) |
| A.8 Technological | A.8.1 – A.8.34 | 34 | Deterministic |
## Control attributes (2022 addition)
Every control carries five attribute dimensions. Surface these in dashboards:
1. **Control type**: preventive, detective, corrective.
2. **Information security property**: confidentiality, integrity, availability.
3. **Cybersecurity concept**: identify, protect, detect, respond, recover.
4. **Operational capability**: governance, asset management, identity and access management, etc.
5. **Security domain**: governance and ecosystem, protection, defense, resilience.
---
## A.5 Organizational Controls (37 controls)
Primary verification mode: agentic. Most A.5 controls are evaluated by LLM review of policy artifacts. A subset has deterministic anchors (e.g., A.5.7 Threat Intelligence is deterministically anchored by the presence of an SCA tool subscription).
| ID | Title | Verification logic |
|---|---|---|
| A.5.1 | Policies for information security | Agentic — verify InfoSec_Policy.md exists, includes commitment to applicable requirements + continual improvement, signed by top management. |
| A.5.2 | Information security roles and responsibilities | Agentic — RACI matrix or equivalent in policy documents. |
| A.5.3 | Segregation of duties | Agentic + deterministic — policy text plus IAM role separation in IaC. |
| A.5.4 | Management responsibilities | Agentic — leadership commitment language in policy. |
| A.5.5 | Contact with authorities | Agentic — incident response plan references regulators (e.g., IMY for Sweden, Datainspektionen). |
| A.5.6 | Contact with special interest groups | Agentic — documented memberships (CERT, ISAC, etc.). |
| A.5.7 | Threat Intelligence (NEW 2022) | Deterministic-anchored — presence of SCA tooling (Trivy, Snyk, Dependabot) actively pulling vuln databases. |
| A.5.8 | Information security in project management | Agentic — project lifecycle docs reference security gates. |
| A.5.9 | Inventory of information and other associated assets | Deterministic — asset inventory file, CMDB reference, or IaC as the inventory source. |
| A.5.10 | Acceptable use of information and other associated assets | Agentic — AUP exists and is acknowledged. |
| A.5.11 | Return of assets | Out-of-repo — pointer to HR offboarding workflow. |
| A.5.12 | Classification of information | Agentic — classification scheme documented; deterministic spot-checks for classification mark-up in files. |
| A.5.13 | Labelling of information | Deterministic — regex check for classification labels in document headers. |
| A.5.14 | Information transfer | Agentic — transfer policy; deterministic — TLS enforcement in IaC. |
| A.5.15 | Access control | Agentic + deterministic — policy + IAM IaC + Steampipe runtime queries. |
| A.5.16 | Identity management | Deterministic — IdP integration verified via Steampipe. |
| A.5.17 | Authentication information | Deterministic — secret manager references in IaC; no hardcoded credentials. |
| A.5.18 | Access rights | Deterministic — least-privilege IAM policies; periodic access review evidence. |
| A.5.19 | Information security in supplier relationships | Agentic — supplier policy and contracts with security clauses. |
| A.5.20 | Addressing information security within supplier agreements | Agentic — DPA/security addendum templates. |
| A.5.21 | Managing information security in the ICT supply chain | Agentic + deterministic — SBOM generation in CI/CD. |
| A.5.22 | Monitoring, review and change management of supplier services | Agentic — supplier review cadence documented. |
| A.5.23 | Information security for use of cloud services (NEW 2022) | Deterministic — IaC alignment with cloud provider security baselines (CIS AWS, CIS Azure). |
| A.5.24 | Information security incident management planning and preparation | Agentic — incident response plan exists. |
| A.5.25 | Assessment and decision on information security events | Agentic — triage criteria documented. |
| A.5.26 | Response to information security incidents | Agentic — runbooks exist. |
| A.5.27 | Learning from information security incidents | Agentic — post-mortem template / blameless review process. |
| A.5.28 | Collection of evidence | Agentic — forensic procedures documented. |
| A.5.29 | Information security during disruption | Agentic — BCP exists. |
| A.5.30 | ICT readiness for business continuity (NEW 2022) | Deterministic — multi-region/multi-AZ in IaC, backup automation. |
| A.5.31 | Legal, statutory, regulatory and contractual requirements | Agentic — compliance register exists and lists applicable laws. |
| A.5.32 | Intellectual property rights | Agentic — IP policy; deterministic — license scanner output. |
| A.5.33 | Protection of records | Agentic — records retention schedule. |
| A.5.34 | Privacy and protection of PII | Agentic — privacy policy; deterministic — data masking in code (links to A.8.11). |
| A.5.35 | Independent review of information security | Agentic — internal audit log shows independent reviewers. |
| A.5.36 | Compliance with policies, rules and standards for information security | Agentic — compliance assessment reports. |
| A.5.37 | Documented operating procedures | Agentic — runbook directory exists. |
---
## A.6 People Controls (8 controls)
Primary verification mode: out-of-repo. Verify policy + evidence pointer to HRIS / LMS / ticketing system.
| ID | Title | Verification logic |
|---|---|---|
| A.6.1 | Screening | Out-of-repo — policy + pointer to HR vetting workflow. |
| A.6.2 | Terms and conditions of employment | Out-of-repo — policy + pointer to contract templates with confidentiality clauses. |
| A.6.3 | Information security awareness, education and training | Out-of-repo — policy + pointer to LMS API showing completion records. |
| A.6.4 | Disciplinary process | Out-of-repo — policy + pointer to HR escalation procedure. |
| A.6.5 | Responsibilities after termination or change of employment | Out-of-repo — policy + pointer to offboarding checklist. |
| A.6.6 | Confidentiality or non-disclosure agreements | Out-of-repo — NDA template exists in repo or pointer. |
| A.6.7 | Remote working | Agentic — remote work policy; deterministic — VPN/zero-trust enforcement in network config. |
| A.6.8 | Information security event reporting | Agentic — reporting channel documented. |
---
## A.7 Physical Controls (14 controls)
Primary verification mode: out-of-repo. Cannot be verified from repository alone. The skill verifies that the policy exists and references the system of record (facility management, badge system, environmental monitoring).
| ID | Title | Verification logic |
|---|---|---|
| A.7.1 | Physical security perimeters | Out-of-repo — policy + facility diagram pointer. |
| A.7.2 | Physical entry | Out-of-repo — policy + badge system reference. |
| A.7.3 | Securing offices, rooms and facilities | Out-of-repo — policy. |
| A.7.4 | Physical security monitoring (NEW 2022) | Out-of-repo — policy + CCTV/access log pointer. |
| A.7.5 | Protecting against physical and environmental threats | Out-of-repo — policy + insurance + environmental monitoring. |
| A.7.6 | Working in secure areas | Out-of-repo — policy. |
| A.7.7 | Clear desk and clear screen | Out-of-repo — policy + screen lock GPO/MDM enforcement. |
| A.7.8 | Equipment siting and protection | Out-of-repo — policy. |
| A.7.9 | Security of assets off-premises | Out-of-repo — laptop/mobile policy + MDM pointer. |
| A.7.10 | Storage media | Out-of-repo — media handling policy. |
| A.7.11 | Supporting utilities | Out-of-repo — UPS/HVAC documentation. |
| A.7.12 | Cabling security | Out-of-repo — facility documentation. |
| A.7.13 | Equipment maintenance | Out-of-repo — maintenance log pointer. |
| A.7.14 | Secure disposal or re-use of equipment | Out-of-repo — disposal policy + certificates of destruction pointer. |
---
## A.8 Technological Controls (34 controls)
Primary verification mode: deterministic. This is where CI/CD scanner ROI is highest.
| ID | Title | Verification logic |
|---|---|---|
| A.8.1 | User endpoint devices | Deterministic — MDM integration scripts; agentic — endpoint policy. |
| A.8.2 | Privileged access rights | Deterministic — IAM IaC scanned for wildcard `Action: "*"` / `Resource: "*"`; Steampipe runtime check for over-privileged roles. |
| A.8.3 | Information access restriction | Deterministic — RBAC middleware / route-level access control in app code. |
| A.8.4 | Access to source code | Deterministic — branch protection rules in `.github/settings.yml`, mandatory code review, no direct push to main. |
| A.8.5 | Secure authentication | Deterministic — MFA enforced in IdP config; HTTP basic auth deprecated; cookie/session security flags set. |
| A.8.6 | Capacity management | Deterministic — autoscaling group definitions, resource limits in K8s manifests. |
| A.8.7 | Protection against malware | Deterministic — endpoint protection deployment; container image vuln scanning (Trivy). |
| A.8.8 | Management of technical vulnerabilities | Deterministic — SCA executes on every PR; CVE threshold enforced. |
| A.8.9 | Configuration management (NEW 2022) | Deterministic — all infrastructure defined as code; drift detection. |
| A.8.10 | Information deletion (NEW 2022) | Deterministic — lifecycle/retention policies in storage IaC. |
| A.8.11 | Data masking (NEW 2022) | Deterministic — static analysis confirming hashing/tokenization on PII handlers. |
| A.8.12 | Data leakage prevention (NEW 2022) | Deterministic — egress filtering rules, restrictive CORS, no public buckets. |
| A.8.13 | Information backup | Deterministic — automated snapshot schedules, backup vault config. |
| A.8.14 | Redundancy of information processing facilities | Deterministic — multi-AZ / multi-region in IaC. |
| A.8.15 | Logging | Deterministic — log aggregation deployed; native cloud audit logs (CloudTrail) enabled. |
| A.8.16 | Monitoring activities (NEW 2022) | Deterministic — alerting rules + webhooks defined. |
| A.8.17 | Clock synchronization | Deterministic — NTP service uniformly configured. |
| A.8.18 | Use of privileged utility programs | Deterministic — no `--privileged` containers; agentic — utility access policy. |
| A.8.19 | Installation of software on operational systems | Deterministic — immutable infrastructure pattern enforced; CI/CD is the only path to production. |
| A.8.20 | Network security | Deterministic — TLS 1.2+ minimums, WAF deployment in load balancers. |
| A.8.21 | Security of network services | Deterministic — service definitions declare security profiles. |
| A.8.22 | Segregation of networks | Deterministic — VPC subnets, route tables, K8s NetworkPolicy enforce isolation. |
| A.8.23 | Web filtering (NEW 2022) | Deterministic — proxy / DNS firewall allow-deny lists. |
| A.8.24 | Use of cryptography | Deterministic — encryption-at-rest flags, encryption-in-transit, no MD5/SHA-1, no hardcoded keys. |
| A.8.25 | Secure development life cycle | Deterministic — Dev/Test/Prod environment isolation; security gates in pipeline. |
| A.8.26 | Application security requirements | Agentic — review of ADRs / design docs for security constraints. |
| A.8.27 | Secure system architecture and engineering principles | Agentic — review of architecture docs for zero-trust / defense-in-depth. |
| A.8.28 | Secure coding (NEW 2022) | Deterministic — SAST in CI/CD scanning OWASP Top 10. |
| A.8.29 | Security testing in development and acceptance | Deterministic — DAST + SAST mandatory pipeline steps. |
| A.8.30 | Outsourced development | Deterministic — SCA on third-party libs; agentic — vendor management policy. |
| A.8.31 | Separation of development, test and production environments | Deterministic — IaC variable files / account separation. |
| A.8.32 | Change management | Deterministic — Git PR approval requirements; CODEOWNERS enforcement. |
| A.8.33 | Test information | Deterministic — test directories scanned for unsanitized production data. |
| A.8.34 | Protection of information systems during audit testing | Deterministic — read-only roles for scanners; no destructive test executions. |
---
## How to use this catalog
1. The SoA tells you which subset applies. Filter this catalog by the SoA before scanning.
2. For each in-scope control, route to the verification mode column.
3. Deterministic controls map to specific Checkov / Trivy / Steampipe checks — see `tool-orchestration.md`.
4. Agentic controls map to LLM prompts — see `agentic-prompts.md`.
5. Out-of-repo controls require evidence pointer verification only.
@@ -0,0 +1,150 @@
# Cross-Framework Mapping — NIST 800-53, CIS Controls, SOC 2
A single ISO 27001 finding can be projected onto NIST SP 800-53 Rev. 5, CIS Controls v8.1, and SOC 2 Trust Services Criteria. Embedding these mappings into the scanner's data model lets a single scan produce dual- or quad-compliance reports.
## Why these three
| Framework | Purpose | Audit type | Best paired with ISO 27001 when |
|---|---|---|---|
| NIST SP 800-53 Rev. 5 | U.S. federal control catalog (highly prescriptive, granular) | Self-assessment / FedRAMP | Selling to U.S. government / FedRAMP authorization. |
| CIS Controls v8.1 | Prioritized, technical implementation roadmap | Operational benchmark | Used as the technical companion to ISO's governance focus. |
| SOC 2 Trust Services Criteria | Operational effectiveness attestation | Type I (point-in-time) or Type II (period) | Selling SaaS to U.S. enterprise customers. |
ISO 27001 is governance-first; CIS is technique-first; NIST is exhaustive-first; SOC 2 is operational-effectiveness-first. They overlap heavily but are not interchangeable.
## ISO 27001:2022 → NIST SP 800-53 Rev. 5
The official NIST mapping uses an asterisk convention: an unmarked mapping means full equivalence; an asterisk means the ISO control does not fully satisfy the NIST control's intent.
| ISO 2022 ID | NIST Control(s) | Equivalence |
|---|---|---|
| A.5.1 (Policies) | PL-1, PM-1 | Full |
| A.5.2 (Roles) | PS-2, AC-1 | Full |
| A.5.7 (Threat Intelligence) | PM-15, PM-16, RA-3, SI-5 | Partial — NIST is more prescriptive on TI sources |
| A.5.9 (Inventory) | CM-8, PM-5 | Partial — NIST CM-8 demands more frequent reconciliation |
| A.5.15 (Access control) | AC-1, AC-2, AC-3, AC-5, AC-6 | Full |
| A.5.23 (Cloud security) | SA-9 (External system services), CA-3 | Partial |
| A.5.30 (BC readiness) | CP-2, CP-7, CP-10 | Full |
| A.5.34 (Privacy / PII) | PT-1 through PT-8 (Privacy family) | **Significant gap — see below** |
| A.6.1 (Screening) | PS-3 | Full |
| A.6.3 (Awareness) | AT-1, AT-2 | Full |
| A.7.1 (Physical perimeter) | PE-3 | Full |
| A.7.4 (Physical monitoring) | PE-6 | Full |
| A.8.2 (Privileged access) | AC-6 (Least privilege) | Full |
| A.8.3 (Information access restriction) | AC-3 | Full |
| A.8.4 (Source code access) | CM-5, AC-3 | Full |
| A.8.8 (Vulnerability management) | RA-5, SI-2 | Full |
| A.8.9 (Configuration management) | CM-2, CM-3, CM-6 | Full |
| A.8.10 (Information deletion) | MP-6, SI-12 | Full |
| A.8.11 (Data masking) | SC-28, SI-19 | Partial |
| A.8.12 (DLP) | SC-7 (Boundary protection), SC-8 | Partial |
| A.8.15 (Logging) | AU-2, AU-3, AU-12 | Full |
| A.8.16 (Monitoring) | AU-6, SI-4 | Full |
| A.8.20 (Network security) | SC-7, SC-8 | Full |
| A.8.22 (Network segregation) | SC-7(13), SC-32 | Full |
| A.8.24 (Cryptography) | SC-13, SC-28, IA-7 | Full |
| A.8.25 (Secure SDLC) | SA-3, SA-15 | Full |
| A.8.28 (Secure coding) | SA-11, SI-10 | Full |
| A.8.32 (Change management) | CM-3, CM-4 | Full |
### Critical NIST gap — privacy
NIST SP 800-53 Rev. 5 integrates a full Privacy (PT) control family directly into the catalog. ISO 27001 addresses privacy primarily through A.5.34 and points to ISO 27701 for full privacy management. Organizations claiming dual ISO 27001 + NIST 800-53 compliance must either:
1. Adopt ISO 27701 in addition, OR
2. Implement the NIST PT family separately and document this as a SoA inclusion above and beyond Annex A.
The scanner should explicitly flag NIST PT family as a documentation gap when generating dual-compliance reports based solely on ISO 27001.
---
## ISO 27001:2022 → CIS Controls v8.1
CIS Controls are organized into 18 numbered controls (formerly "Critical Security Controls"). They are the most operationally prescriptive of the four frameworks.
| ISO 2022 | CIS Control | Notes |
|---|---|---|
| A.5.9 (Inventory of assets) | CIS 1 (Enterprise Assets), CIS 2 (Software Assets) | CIS provides the technical methodology. |
| A.8.10, A.8.11, A.8.12 (Data lifecycle) | CIS 3 (Data Protection) | Direct alignment. |
| A.8.9 (Configuration management) | CIS 4 (Secure Configuration) | Direct alignment. |
| A.5.15, A.8.2 (Access control) | CIS 5 (Account Management), CIS 6 (Access Control Management) | Split across two CIS controls. |
| A.8.8 (Vulnerability management) | CIS 7 (Continuous Vulnerability Management) | Direct alignment. |
| A.8.15 (Logging) | CIS 8 (Audit Log Management) | Direct alignment. |
| A.8.7 (Malware protection) | CIS 10 (Malware Defenses) | Direct alignment. |
| A.8.13 (Backup) | CIS 11 (Data Recovery) | Direct alignment. |
| A.8.20, A.8.22 (Network security) | CIS 12 (Network Infrastructure Management), CIS 13 (Network Monitoring) | Split. |
| A.6.3 (Awareness training) | CIS 14 (Security Awareness and Skills Training) | Direct alignment. |
| A.5.21, A.8.30 (Supply chain) | CIS 15 (Service Provider Management) | Direct alignment. |
| A.8.25, A.8.28 (SDLC, secure coding) | CIS 16 (Application Software Security) | Direct alignment. |
| A.5.24-28 (Incident management) | CIS 17 (Incident Response Management) | Direct alignment. |
| A.8.29 (Security testing) | CIS 18 (Penetration Testing) | Direct alignment. |
CIS Controls have three Implementation Groups (IG1, IG2, IG3) of increasing rigor. The scanner should report which IG level each in-scope CIS control reaches based on detected configurations.
---
## ISO 27001:2022 → SOC 2 Trust Services Criteria
SOC 2 has five categories. Security (Common Criteria, CC) is mandatory for every SOC 2 report; the others are optional based on the service's commitments.
| TSC Category | When to include |
|---|---|
| Security (CC1-CC9) | Always. |
| Availability | If uptime is part of customer commitments. |
| Processing Integrity | If accuracy/completeness of processing matters (e.g., financial systems). |
| Confidentiality | If protecting non-personal confidential data (trade secrets, customer business data). |
| Privacy | If processing personal information. |
### ISO → SOC 2 Common Criteria mapping
| ISO 2022 | SOC 2 CC | Notes |
|---|---|---|
| A.5.1 (Policies) | CC1.1, CC2.2 | Control environment foundation. |
| Clause 5 (Leadership) | CC1.1, CC1.2, CC1.3 | Tone at the top. |
| A.5.2 (Roles) | CC1.4, CC1.5 | Accountability. |
| A.5.4 (Management responsibilities) | CC2.1, CC2.2 | Communication. |
| Clause 6.1 (Risk) | CC3.1, CC3.2, CC3.3, CC3.4 | Risk assessment. |
| Clause 9 (Performance evaluation) | CC4.1, CC4.2 | Monitoring. |
| A.5.1, Clause 5.2 | CC5.1, CC5.2, CC5.3 | Control activities. |
| A.5.15, A.5.16, A.8.2, A.8.3, A.8.5 | CC6.1, CC6.2, CC6.3 | Logical access. |
| A.7.1-A.7.14 | CC6.4, CC6.5 | Physical access. |
| A.8.10 | CC6.5 | Information disposal. |
| A.8.24 | CC6.6, CC6.7, CC6.8 | Data protection in transit and at rest. |
| A.8.15, A.8.16 | CC7.1, CC7.2 | System operations. |
| A.5.24-A.5.28 | CC7.3, CC7.4, CC7.5 | Incident response. |
| A.8.32 | CC8.1 | Change management. |
| Clause 10 (Improvement), A.5.21 | CC9.1, CC9.2 | Risk mitigation, vendor management. |
### Dual-report generation
When generating a SOC 2 + ISO 27001 dual report, the scanner should:
1. Run the ISO 27001 scan as primary.
2. Project each finding onto the corresponding SOC 2 CC.
3. Highlight ISO controls that have **no SOC 2 equivalent** (rare, but exists for some governance-heavy A.5 controls).
4. Highlight SOC 2 CCs that are **only partially covered** by ISO controls (e.g., processing integrity and privacy categories require additional evidence).
---
## Implementation in scanner data model
Each finding object should carry a `framework_mappings` field:
```json
{
"finding_id": "F-001",
"iso_27001_2022": ["A.8.24"],
"nist_800_53_r5": ["SC-13", "SC-28"],
"cis_v8_1": ["3.11"],
"soc_2_tsc": ["CC6.6", "CC6.7"],
"severity": "high",
"title": "EBS volume not encrypted at rest"
}
```
This allows the same finding to appear in four different report views without re-running the scan.
## Caveats
- Mappings are bidirectional but lossy. ISO is governance-heavy; NIST is technical-heavy; CIS is implementation-heavy; SOC 2 is operational-effectiveness-heavy. Different frameworks emphasize different aspects of the same underlying risk.
- An organization passing one framework does not automatically pass another. Each framework has unique requirements (NIST privacy family, SOC 2 processing integrity, etc.).
- Mappings change between framework versions. Verify the version (ISO 27001:2022, NIST 800-53 Rev. 5, CIS v8.1, SOC 2 2017 TSC) every time. The skill should refuse to map without explicit version assertions.
@@ -0,0 +1,94 @@
# ISMS Clauses 4-10 — Mandatory Documents and Verification Logic
ISO/IEC 27001:2022 is structurally bifurcated. Annex A enumerates security controls; Clauses 4-10 define the management system that must wrap them. Certification fails without a complete and operating Management System, regardless of how well Annex A controls are implemented technically.
The clauses are: Context (4), Leadership (5), Planning (6), Support (7), Operation (8), Performance Evaluation (9), Improvement (10). Each produces required documented information that the agentic auditor must locate and evaluate.
## Mandatory documents per clause
| Clause | Document | Filename convention | Verification logic |
|---|---|---|---|
| 4.3 | Scope of the ISMS | `ISMS_Scope.md` | Must define exact boundaries — included departments, locations, systems, exclusions with justification. |
| 5.2 | Information Security Policy | `InfoSec_Policy.md` | Must include framework for objectives, commitment to satisfy applicable requirements, commitment to continual improvement, top management approval. |
| 6.1.2 | Risk Assessment Process | `Risk_Methodology.md` | Must define scoring matrix (impact × likelihood), risk acceptance thresholds, owner assignment criteria. |
| 6.1.3 | Statement of Applicability | `SoA.json` / `SoA.csv` | Must contain four core elements per control: identifier, inclusion/exclusion statement, justification, implementation status. |
| 6.2 | Information Security Objectives | `Security_Objectives.md` | Must contain measurable, time-bound goals with assigned owners. |
| 7.2 | Evidence of Competence | `Competency_Matrix.csv` | Records of training, qualifications, certifications. Cross-reference with HRIS where possible. |
| 7.5 | Documented Information control | (cross-cutting) | Every document must have classification mark-up, version table, sign-off, review date within annual interval. |
| 8.2 | Risk Assessment Results | `Risk_Register.xlsx` | Active ledger. Every entry must have risk owner, residual risk score, treatment decision. |
| 8.3 | Risk Treatment Plan | `Risk_Treatment.md` | Must align with Risk Register and SoA. Every accepted risk must be referenced. |
| 9.1 | Performance evaluation results | `KPI_Dashboard.md` or equivalent | Metrics on ISMS effectiveness. |
| 9.2 | Internal Audit Programme + Results | `Internal_Audit_Log.md` | Dates, auditors (independent from auditee), findings, non-conformities, corrective actions. |
| 9.3 | Management Review minutes | `Mgmt_Review_Minutes.md` | Must show executive participation and cover the mandatory inputs (see below). |
| 10.1 | Continual improvement | (cross-cutting) | Evidence of corrective actions closing previous findings. |
| 10.2 | Nonconformity and corrective action records | `NC_Log.md` or `NC_Register.xlsx` | Each NC must have root cause analysis, corrective action, effectiveness verification. |
## Clause 9.3 — Mandatory Management Review inputs
This is the most commonly under-documented requirement. Management Review minutes must explicitly cover **all** of the following:
1. Status of actions from previous management reviews.
2. Changes in external and internal issues relevant to the ISMS.
3. Changes in needs and expectations of interested parties.
4. Feedback on information security performance, including:
a. Nonconformities and corrective actions.
b. Monitoring and measurement results.
c. Audit results.
d. Fulfilment of information security objectives.
5. Feedback from interested parties.
6. Results of risk assessment and status of risk treatment plan.
7. Opportunities for continual improvement.
The agentic auditor must verify each of these is textually present. Output a per-input checklist showing pass/fail for each agenda item.
## Clause 7.5 — Documented Information requirements
Every controlled document must satisfy:
| Requirement | Verification logic |
|---|---|
| Title and identifier | Regex check for unique document ID. |
| Date of issue / last revision | Regex check + freshness check (within annual interval). |
| Version number | Regex check for semantic versioning or sequential numbering. |
| Classification mark-up | Regex search for "Confidential", "Internal Use Only", "Public" tokens. |
| Author / approver | Regex check for sign-off section. |
| Approval evidence | Cross-reference with version control history (Git blame for last approver). |
Documents missing any of these elements are deterministic violations of Clause 7.5, separate from any content-quality issues.
## Document quality vs document existence
The agentic auditor must distinguish two failure modes:
1. **Existence failure**: Document is missing entirely. Deterministic.
2. **Adequacy failure**: Document exists but content does not meet the clause requirements. Probabilistic, requires LLM evaluation.
Adequacy failures are more common than existence failures in mature organizations. A `Mgmt_Review_Minutes.md` may exist as a file but fail to mention 5 of the 7 mandatory Clause 9.3 inputs. The deterministic check passes; the agentic check fails.
Always run both checks and report them separately.
## Boilerplate detection
Be especially alert to AI-generated boilerplate. Common red flags:
- Generic language with no organizational specificity (no team names, no system names, no actual numbers).
- Missing contextual references to the organization's actual systems, products, or geography.
- Inconsistent voice or tense across sections.
- Identical phrasing to template repositories on GitHub.
Boilerplate documents technically satisfy existence checks but fail adequacy. Flag as Observation, not Non-Conformity, since they can be remediated by genuine documentation work.
## Cross-clause consistency checks
The auditor must run cross-references:
| Check | Source A | Source B | Failure mode |
|---|---|---|---|
| Risk Register entries → SoA controls | Risk_Register | SoA | Risk references control X but SoA excludes X without justification linking to compensating controls. |
| Risk Treatment Plan → Risk Register | Risk_Treatment | Risk_Register | Treatment references risk ID not in register. |
| SoA inclusions → InfoSec_Policy | SoA | InfoSec_Policy | Policy claims commitments not reflected in SoA inclusions. |
| Internal Audit findings → NC Log | Internal_Audit_Log | NC_Log | Audit identified NC but no entry in NC log. |
| Competency Matrix → InfoSec roles | Competency_Matrix | Roles defined in policies | Role exists in policy but no competency requirement defined. |
| Suppression Risk IDs → Risk Register | Inline scanner suppressions | Risk_Register | Suppression references Risk ID that does not exist. |
These cross-checks are the highest-value agentic operations because they detect compliance theater — where every individual document looks fine but the documents are internally inconsistent.
@@ -0,0 +1,82 @@
# 2013 → 2022 Control Mapping
When scanning legacy repositories or evaluating SoAs that still reference 2013-era control identifiers, translate using this matrix before generating findings. Failing to translate produces false-positive non-conformities.
## Structural change
| Aspect | 2013 | 2022 |
|---|---|---|
| Control count | 114 | 93 |
| Domains/themes | 14 domains (A.5–A.18) | 4 themes (A.5–A.8) |
| Attribute system | None | 5 attribute dimensions per control |
| Merged controls | — | 57 → 24 |
| New controls | — | 11 net-new |
| Retained (renamed/renumbered) | — | 58 |
## Common translation patterns
The 2013 controls A.5–A.18 do not map cleanly to A.5–A.8. The new themes are reorganized around governance level, not topical similarity to the old domains.
| 2013 ID | 2013 Title | 2022 ID | 2022 Title | Notes |
|---|---|---|---|---|
| A.5.1.1 | Policies for information security | A.5.1 | Policies for information security | Direct rename. |
| A.6.1.1 | Information security roles and responsibilities | A.5.2 | Information security roles and responsibilities | Direct rename. |
| A.6.1.2 | Segregation of duties | A.5.3 | Segregation of duties | Direct rename. |
| A.6.1.3 | Contact with authorities | A.5.5 | Contact with authorities | Direct rename. |
| A.6.1.4 | Contact with special interest groups | A.5.6 | Contact with special interest groups | Direct rename. |
| A.6.2 | Mobile devices and teleworking | A.6.7, A.8.1 | Remote working / User endpoint devices | Split. |
| A.7 | Human resource security (entire domain) | A.6 | People (entire theme) | Restructured. |
| A.8.1.1 | Inventory of assets | A.5.9 | Inventory of information and other associated assets | Renamed, broader. |
| A.8.2 | Information classification | A.5.12, A.5.13 | Classification of information / Labelling | Split into two. |
| A.9 | Access control (entire domain) | A.5.15–A.5.18, A.8.2–A.8.5 | Distributed across themes | Major restructure. |
| A.9.1.1 | Access control policy | A.5.15 | Access control | Merged. |
| A.9.2.1 | User registration and de-registration | A.5.16 | Identity management | Renamed. |
| A.9.2.3 | Management of privileged access rights | A.8.2 | Privileged access rights | Renamed. |
| A.9.4.5 | Access control to program source code | A.8.4 | Access to source code | Renamed. |
| A.10 | Cryptography | A.8.24 | Use of cryptography | Compressed. |
| A.10.1.1 | Policy on the use of cryptographic controls | A.8.24 | Use of cryptography | Merged. |
| A.10.1.2 | Key management | A.8.24 | Use of cryptography | Merged. |
| A.11 | Physical and environmental security | A.7 | Physical (entire theme) | Restructured. |
| A.12.1.1 | Documented operating procedures | A.5.37 | Documented operating procedures | Moved to Organizational. |
| A.12.1.2 | Change management | A.8.32 | Change management | Renumbered. |
| A.12.1.3 | Capacity management | A.8.6 | Capacity management | Renumbered. |
| A.12.1.4 | Separation of development, testing and operational environments | A.8.31 | Separation of development, test and production environments | Renumbered. |
| A.12.2.1 | Controls against malware | A.8.7 | Protection against malware | Renamed. |
| A.12.3.1 | Information backup | A.8.13 | Information backup | Direct rename. |
| A.12.4.1 | Event logging | A.8.15 | Logging | Compressed (4 controls into 1). |
| A.12.4.2 | Protection of log information | A.8.15 | Logging | Merged. |
| A.12.4.3 | Administrator and operator logs | A.8.15 | Logging | Merged. |
| A.12.4.4 | Clock synchronisation | A.8.17 | Clock synchronization | Renumbered. |
| A.12.5.1 | Installation of software on operational systems | A.8.19 | Installation of software on operational systems | Renumbered. |
| A.12.6.1 | Management of technical vulnerabilities | A.8.8 | Management of technical vulnerabilities | Renumbered. |
| A.12.6.2 | Restrictions on software installation | A.8.19 | Installation of software on operational systems | Merged. |
| A.13.1.1 | Network controls | A.8.20 | Network security | Renamed. |
| A.13.1.2 | Security of network services | A.8.21 | Security of network services | Renumbered. |
| A.13.1.3 | Segregation in networks | A.8.22 | Segregation of networks | Renumbered. |
| A.13.2 | Information transfer | A.5.14 | Information transfer | Moved to Organizational. |
| A.14.1 | Security requirements of information systems | A.8.26 | Application security requirements | Compressed. |
| A.14.2.1 | Secure development policy | A.8.25 | Secure development life cycle | **Frequently referenced legacy ID.** |
| A.14.2.2 | System change control procedures | A.8.32 | Change management | Merged. |
| A.14.2.5 | Secure system engineering principles | A.8.27 | Secure system architecture and engineering principles | Renamed. |
| A.14.2.7 | Outsourced development | A.8.30 | Outsourced development | Renumbered. |
| A.14.2.8 | System security testing | A.8.29 | Security testing in development and acceptance | Renamed. |
| A.14.3.1 | Protection of test data | A.8.33 | Test information | Renamed. |
| A.15 | Supplier relationships | A.5.19–A.5.22 | Supplier-related controls | Restructured into Organizational. |
| A.16 | Information security incident management | A.5.24–A.5.28 | Incident-related controls | Restructured. |
| A.17 | Information security aspects of business continuity | A.5.29, A.5.30, A.8.13, A.8.14 | Distributed | Restructured. |
| A.18.1 | Compliance with legal and contractual requirements | A.5.31 | Legal, statutory, regulatory and contractual requirements | Compressed. |
| A.18.1.3 | Protection of records | A.5.33 | Protection of records | Renumbered. |
| A.18.1.4 | Privacy and protection of PII | A.5.34 | Privacy and protection of PII | Renumbered. |
| A.18.2.1 | Independent review of information security | A.5.35 | Independent review of information security | Renumbered. |
## Implementation note for the scanner
When parsing a SoA that uses 2013 identifiers:
1. Detect the format (heuristic: presence of identifiers like A.9.x, A.12.x, A.14.x indicates 2013).
2. Apply the translation table above.
3. Generate a warning: "SoA references 2013-era identifiers. Translation applied. Recommend updating SoA to 2022 identifiers."
4. Run all checks against translated 2022 controls.
5. Verify the 11 net-new controls (see `new-controls-2022.md`) are now addressed in the SoA — they will not have legacy equivalents.
The translation is one-way and lossy. Several 2013 controls map to multiple 2022 controls (splits) and several 2022 controls absorb multiple 2013 controls (merges). When in doubt, apply the most restrictive interpretation.
@@ -0,0 +1,114 @@
# Honest Limitations of Automated ISO 27001:2022 Compliance Verification
The skill is an aid to certification, not a substitute. A scanner that overpromises destroys audit credibility. Surface these limitations in every report.
## Limitation 1 — Physical reality
No code execution can verify physical reality. The scanner can confirm a policy says "server room doors must be locked" (A.7.2) or "premises shall be monitored" (A.7.4). It cannot confirm that any physical door is actually locked or that any actual camera is recording.
**Required disclaimer in reports**:
> "Physical Controls (A.7.x) verified by documentation only. Physical validation requires on-site inspection and is out of scope for repository-based scanning."
## Limitation 2 — Behavioral compliance
Human behavior is opaque to scanners. Several controls require people to actually do things, not just for the documentation to require it.
| Control | What scanner can verify | What scanner cannot verify |
|---|---|---|
| A.6.3 Awareness training | LMS API shows completion | Whether the employee understood the material |
| A.7.7 Clear desk and clear screen | Policy exists, screen lock GPO/MDM enforced | Whether actual desks are clear, whether passwords are written on post-its |
| A.6.6 NDAs | Template exists | Whether all employees have signed |
| A.5.10 Acceptable use | Policy exists | Whether employees follow it |
**Required disclaimer**:
> "Behavioral compliance is out of scope. Verification requires walkthroughs, interviews, and observation by human auditors."
## Limitation 3 — Point-in-time scans vs continuous compliance
A repository can pass all checks at PR-merge time and immediately drift out of compliance through:
- Manual changes via cloud console (clickops).
- Resources created outside IaC (e.g., emergency hotfixes).
- Configuration changes by SaaS vendors (e.g., default settings changing in a managed service).
- Credentials rotated outside the documented process.
This violates A.8.9 (Configuration management) and A.8.16 (Monitoring) by definition. Mitigation requires continuous runtime monitoring (Steampipe queries on a schedule, AWS Config rules, Azure Policy, GCP Organization Policy) — not just CI/CD-time scans.
**Required disclaimer**:
> "This is a point-in-time assessment of commit <sha>. Production state may have drifted from this commit. Continuous runtime monitoring (A.8.16) is required to detect post-merge drift."
## Limitation 4 — Document existence vs adequacy
The scanner can prove a file exists. It can apply LLM evaluation to assess whether the file's content meets normative requirements. It cannot prove that the document is current, that the listed approvers actually approved it, or that the procedures described are actually followed.
The agentic auditor produces "Observations" that require human validation. Adequacy claims are not certifications.
**Required disclaimer**:
> "Agentic policy review is advisory. Findings labeled 'Observation' or 'Potential Non-Conformity' require validation by a qualified human auditor."
## Limitation 5 — Agentic AI in the audited system itself
If the audited system uses AI agents in production (e.g., LLM-based workflows, autonomous decision-making, MCP-server-driven automation), traditional ISO 27001 controls are insufficient.
Issues:
- A.8.28 (Secure coding) was written for deterministic code. LLM behavior is probabilistic.
- A.8.32 (Change management) assumes changes are deployable artifacts. Prompt or retrieval changes may bypass this.
- A.5.7 (Threat intelligence) does not cover prompt injection or jailbreaking.
- A.8.29 (Security testing) does not specify adversarial robustness testing.
Reference: OWASP Top 10 for Agentic Applications, Agentic Trust Framework. The scanner should:
1. Detect presence of agentic AI components (LLM API calls, MCP servers, autonomous agents).
2. Demand additional documented governance for autonomy boundaries.
3. Flag standard SOC 2 / ISO 27001 audit reports as insufficient for autonomous systems.
**Required disclaimer when agentic AI detected**:
> "This system uses agentic AI components. Standard ISO 27001:2022 controls do not adequately address probabilistic and autonomous system behavior. Reference OWASP Top 10 for Agentic Applications and supplement with agent-specific governance controls."
## Limitation 6 — Out-of-repo systems
Many controls depend on systems outside the repository: HRIS, IdP, MDM, ticketing, badge access. The scanner can verify policy + evidence pointers, but not the state of external systems unless explicit API integration is configured (e.g., Steampipe with appropriate plugins).
When external API integration is unavailable, mark the control as "Documentation Verified — System of Record Not Queried" rather than "Verified".
## Limitation 7 — SoA dependency
The scanner is only as accurate as the SoA. A SoA that excludes critical controls without justification will cause the scanner to skip checks that should be running. The agentic auditor must evaluate the SoA's exclusion justifications, but ultimately the SoA reflects organizational decisions that the scanner cannot override.
If the SoA is missing, malformed, or includes obviously inappropriate exclusions, generate a Clause 6.1.3 finding and refuse to issue a "compliant" verdict regardless of technical scan results.
## Limitation 8 — Certification body discretion
ISO 27001 certification is issued by accredited certification bodies, not by automated tools. A clean scanner output is not a certificate. Certification bodies apply professional judgment, conduct interviews, perform sampling, and examine evidence in ways that cannot be fully automated.
The skill produces audit-defensible evidence and identifies probable findings. It does not certify.
**Required language**:
> "This report is internal evidence to support the organization's information security management system. It is not an ISO 27001 certificate. Certification requires audit by an accredited certification body."
## Limitation 9 — Accuracy of cross-framework mappings
NIST 800-53 Rev. 5, CIS Controls v8.1, and SOC 2 TSC are independent frameworks. ISO 27001 mappings are approximations. A control passing in ISO does not automatically pass in another framework. Privacy is the largest gap (see `cross-framework-mapping.md`).
**Required disclaimer when generating dual-framework reports**:
> "Cross-framework mappings are approximations. Each framework has unique requirements that may not be covered by ISO 27001 alone. Specifically, NIST 800-53 Rev. 5 Privacy (PT) family and SOC 2 Privacy criteria require supplementary controls beyond ISO 27001."
## Limitation 10 — False negatives in static analysis
SAST tools have known false-negative rates. Custom detection patterns, novel vulnerabilities, business-logic flaws, and authorization bypasses often escape pattern matching. Achieving high pass rates on the scanner does not prove the absence of vulnerabilities — it proves the absence of detected vulnerabilities.
A.8.29 (Security testing) requires testing methods beyond SAST: DAST, IAST, manual penetration testing, threat modeling. The scanner can verify these are scheduled and that reports exist; it cannot replace them.
---
## Summary table for report generators
| Verification claim | Strength | Required disclaimer |
|---|---|---|
| "EBS volume encrypted (A.8.24)" | Strong | None for the IaC fact; add point-in-time disclaimer for the runtime state. |
| "Branch protection enabled (A.8.4)" | Strong | None. |
| "Information Security Policy adequate (Clause 5.2)" | Moderate | Agentic / human review required. |
| "Physical perimeter secured (A.7.1)" | Weak | Documentation only. Physical validation required. |
| "Awareness training completed (A.6.3)" | Weak | LMS records verify completion, not comprehension. |
| "Configuration drift detected" | Strong | Requires Steampipe runtime querying. |
| "ISO 27001:2022 compliant" | **Never claim this** | Certification is the certification body's determination. |
The skill can claim "evidence of conformance with <specific clauses or controls>". It cannot claim "compliant".
@@ -0,0 +1,85 @@
# 11 New Controls Introduced in ISO/IEC 27001:2022
The 2022 revision condensed 114 controls (in 14 domains) down to 93 controls (in 4 themes). 57 legacy controls were merged into 24 modern equivalents, 58 were retained with minor updates, and 11 were introduced net-new. The 11 net-new controls are the most likely automation gaps when transitioning from a 2013-era compliance program.
The October 31, 2025 transition deadline has now passed, so any active certification must be against the 2022 standard. Treat absence of these 11 controls in a SoA as a presumptive transition failure.
## The 11 new controls
### A.5.7 Threat Intelligence
**Intent**: Collect and analyze information about threats to inform risk treatment.
**Repository signal**: Active SCA tooling (Trivy, Snyk, Dependabot, Mend) with continuously updated vulnerability databases. Threat-intel feed subscriptions documented.
**Common gap**: Organizations have a vuln scanner but no documented threat-intel ingestion process. The control requires both.
### A.5.23 Information security for use of cloud services
**Intent**: Specify, manage, and review security requirements for acquiring and using cloud services.
**Repository signal**: IaC templates aligned with cloud provider security baselines (CIS AWS Foundations Benchmark, CIS Azure Benchmark, CIS GCP Benchmark). Cloud account governance documented (landing zones, organizational policies).
**Common gap**: Multi-account / multi-cloud deployments without a documented cloud security strategy. SoA inclusion is mandatory for any organization using IaaS/PaaS.
### A.5.30 ICT readiness for business continuity
**Intent**: Plan, implement, maintain, and test ICT readiness based on business continuity objectives.
**Repository signal**: Multi-AZ / multi-region deployment in IaC. Backup automation. Documented RTO/RPO. Disaster recovery runbooks. DR test evidence.
**Common gap**: Backups exist but restore procedures are never tested. The control requires evidence of testing.
### A.7.4 Physical security monitoring
**Intent**: Monitor premises continuously for unauthorized physical access.
**Repository signal**: Out-of-repo. Verify policy + pointer to CCTV / access log system. For fully remote organizations, document the exclusion in the SoA with justification.
**Common gap**: Organization claims "we are remote-first" but employees still have home offices with company equipment. Document the boundary.
### A.8.9 Configuration management
**Intent**: Establish, document, implement, monitor, and review configurations of hardware, software, services, and networks.
**Repository signal**: All infrastructure defined as code in version control. Drift detection in pipelines (terraform plan against deployed state). Configuration baselines for OS images / container base images.
**Common gap**: Production resources created via console clickops alongside Terraform-managed resources. The scanner must detect this divergence.
### A.8.10 Information deletion
**Intent**: Ensure information is deleted when no longer required to prevent unauthorized exposure and to meet legal, regulatory, and contractual requirements.
**Repository signal**: S3 lifecycle rules, database TTLs, log retention policies, GDPR right-to-erasure handlers in application code.
**Common gap**: Test/staging environments with stale production-derived data sitting indefinitely.
### A.8.11 Data masking
**Intent**: Use data masking, pseudonymization, or anonymization to protect sensitive information.
**Repository signal**: PII handlers in application code use hashing, tokenization, or format-preserving encryption. Test fixtures use synthetic data. Logging frameworks redact sensitive fields before output.
**Common gap**: Logs include full payloads of API requests with PII intact.
### A.8.12 Data leakage prevention
**Intent**: Apply DLP measures to systems, networks, and devices that process, store, or transmit sensitive information.
**Repository signal**: Egress filtering in security groups / network policies. Restrictive CORS configurations. No public S3/GCS/Azure Blob buckets. Email DLP integration where applicable.
**Common gap**: Unrestricted egress to the internet from production VPCs.
### A.8.16 Monitoring activities
**Intent**: Monitor networks, systems, and applications for anomalous behavior to detect potential information security incidents.
**Repository signal**: Alerting rules deployed via IaC. SIEM integration. Webhook integrations to incident response tooling (PagerDuty, Opsgenie). Anomaly detection on key metrics.
**Common gap**: Logging exists (A.8.15) but nobody is alerted on anomalies. A.8.15 and A.8.16 are distinct — implement both.
### A.8.23 Web filtering
**Intent**: Manage access to external websites to reduce exposure to malicious content.
**Repository signal**: DNS firewall configuration (Cloudflare Gateway, Cisco Umbrella, AWS Route 53 Resolver DNS Firewall). Proxy allow/deny lists. Outbound HTTP egress restricted.
**Common gap**: Endpoint-level web filtering exists but server-side egress is unrestricted.
### A.8.28 Secure coding
**Intent**: Apply secure coding principles to reduce vulnerabilities introduced during development.
**Repository signal**: SAST tooling (Semgrep, SonarQube, CodeQL) in CI/CD. Linter rules for security patterns. Pre-commit hooks. Documented secure coding standards.
**Common gap**: SAST runs but findings are never blocking. The control requires that findings actually gate merges.
## SoA implications
If transitioning from 2013, every SoA must explicitly address these 11 controls. The reasonable defaults for inclusion / exclusion:
| Control | Default | Notes |
|---|---|---|
| A.5.7 | Include | Almost no organization can credibly exclude threat intelligence. |
| A.5.23 | Include if any cloud usage | Exclude only for strictly on-prem orgs. |
| A.5.30 | Include | Business continuity is universal. |
| A.7.4 | Conditional exclusion if remote-only | Document boundary clearly. |
| A.8.9 | Include | If you have any IT infrastructure, this applies. |
| A.8.10 | Include | Data lifecycle is universal. |
| A.8.11 | Conditional | Exclude only if processing zero PII (rare). |
| A.8.12 | Include | Universal. |
| A.8.16 | Include | Universal. |
| A.8.23 | Conditional | Exclude only if all egress is fundamentally restricted by architecture. |
| A.8.28 | Include | Mandatory for any organization developing software. |
Suspicious patterns the auditor should flag:
- More than 2 of these excluded → likely incomplete transition.
- All included as "Planned" → SoA is aspirational rather than reflective of actual state.
- Any included as "Implemented" without corresponding repository evidence → false claim.
@@ -0,0 +1,289 @@
# Tool Orchestration — Checkov, Trivy, Steampipe
The skill must not reimplement what mature open-source security tools already do. Wrap them. The three primary engines together cover the full repository-and-runtime surface for ISO 27001:2022.
| Tool | Domain | Typical input | Output |
|---|---|---|---|
| Checkov | IaC static analysis | Terraform, CloudFormation, Kubernetes, ARM, Helm, Dockerfile | JSON / SARIF / JUnit |
| Trivy | SCA, container, secret scanning | Filesystem, container images, dependency manifests | JSON / SARIF / table |
| Steampipe | Live runtime querying | Cloud APIs, IdP APIs, GitHub API | SQL result sets |
Checkov and Trivy answer "what does the code say?" Steampipe answers "what is actually deployed?" Both questions matter — IaC drift means the code may be compliant while production isn't.
---
## Checkov
### Why
Checkov has the deepest IaC coverage of any open-source scanner: ~1,000 built-in policies across AWS, Azure, GCP, Kubernetes, Docker, Helm, and more. It uses a graph-based engine that can evaluate cross-resource relationships (e.g., "this RDS is in a subnet whose route table allows 0.0.0.0/0").
### Mapping built-in policies to ISO 27001
Checkov's built-in policies are CIS-aligned. To wrap them for ISO 27001:2022 the skill needs a translation layer:
```yaml
# .checkov.yaml or via custom config
iso_27001_mapping:
CKV_AWS_3: # EBS volume encryption
iso_controls: ["A.8.24"]
CKV_AWS_16: # RDS encryption
iso_controls: ["A.8.24"]
CKV_AWS_19: # S3 server-side encryption
iso_controls: ["A.8.24", "A.8.10"]
CKV_AWS_24: # SSH from 0.0.0.0/0
iso_controls: ["A.8.20", "A.8.22"]
CKV_AWS_46: # IAM hardcoded AWS access keys
iso_controls: ["A.8.24", "A.5.17"]
CKV_AWS_53: # S3 block public ACLs
iso_controls: ["A.8.12", "A.8.3"]
# ... and so on
```
The wrapper script:
1. Runs `checkov -d ./repo --output json --quiet`.
2. Parses the JSON output.
3. Joins each `check_id` against the mapping table.
4. Re-emits findings tagged with ISO 27001 control identifiers.
5. Filters out findings for controls excluded by the SoA.
### Custom policies for ISO-specific patterns
Checkov supports custom policies in YAML for simple attribute checks and Python for graph-based checks. Example custom policy for A.8.24 — ensuring all EBS volumes are encrypted:
```yaml
metadata:
name: "Ensure EBS volumes are encrypted (ISO 27001 A.8.24)"
id: "CUSTOM_ISO_A824_EBS_ENCRYPTION"
category: "ENCRYPTION"
definition:
cond_type: "attribute"
resource_types:
- "aws_ebs_volume"
attribute: "encrypted"
operator: "equals"
value: true
```
Place custom policies in `.checkov/custom_policies/` and run with `--external-checks-dir`.
### Suppression integration
Checkov inline suppressions (`# checkov:skip=<CHECK_ID>: <reason>`) must be parsed by the wrapper to extract Risk Register IDs. Recommended convention:
```hcl
# checkov:skip=CKV_AWS_19: Risk accepted by CISO 2026-05-01 (Risk-ID: SEC-102)
resource "aws_s3_bucket" "legacy_export" {
# ...
}
```
The wrapper extracts `Risk-ID: SEC-102` and verifies SEC-102 exists in the canonical Risk Register before honoring the suppression.
---
## Trivy
### Why
Trivy is the de-facto standard for container and dependency scanning. It supports filesystem scans, container images, Git repos, Kubernetes clusters, AWS environments, and SBOM generation. Its `--compliance` flag enables custom compliance specs.
### Custom ISO 27001 compliance spec
Create `trivy-iso27001-2022.yaml`:
```yaml
spec:
id: iso-27001-2022
title: ISO/IEC 27001:2022
description: Custom compliance spec mapping Trivy checks to ISO 27001:2022 Annex A
relatedResources: []
version: "1.0"
controls:
- id: A.8.8
name: Management of technical vulnerabilities
description: Information about technical vulnerabilities of information systems in use shall be obtained, the organization's exposure to such vulnerabilities shall be evaluated and appropriate measures shall be taken.
checks:
- id: AVD-DS-0001 # Trivy container vulnerability check
- id: CVE-* # All CVE detection
severity: HIGH
- id: A.8.24
name: Use of cryptography
description: Rules for the effective use of cryptography, including cryptographic key management, shall be defined and implemented.
checks:
- id: secret-* # All secret detection rules
severity: CRITICAL
- id: A.5.17
name: Authentication information
description: Allocation and management of authentication information shall be controlled.
checks:
- id: secret-*
severity: CRITICAL
- id: A.8.30
name: Outsourced development
description: The organization shall direct, monitor and review the activities related to outsourced system development.
checks:
- id: AVD-DS-* # Dependency CVEs
severity: HIGH
```
Run:
```bash
trivy fs ./repo --compliance @trivy-iso27001-2022.yaml --format json
```
### What Trivy covers for ISO
| ISO Control | Trivy capability |
|---|---|
| A.8.8 (Vuln management) | OS package CVEs, language-specific CVEs (npm, pip, gem, etc.) |
| A.8.24 (Cryptography) | Secret detection (AWS keys, GitHub tokens, generic high-entropy strings) |
| A.5.17 (Authentication info) | Secret detection in source code |
| A.8.30 (Outsourced dev) | Third-party library CVEs |
| A.8.7 (Anti-malware) | Container image scanning for known malicious packages |
| A.8.21 (Network service security) | Misconfigurations in K8s services and Dockerfile exposed ports |
### SBOM generation for A.5.21
Trivy can generate CycloneDX or SPDX SBOMs:
```bash
trivy fs --format cyclonedx --output sbom.json ./repo
```
SBOM generation directly evidences A.5.21 (Managing information security in the ICT supply chain) and A.8.30.
---
## Steampipe
### Why
Checkov and Trivy operate on what's in the repo. Steampipe queries what's actually deployed. Drift between IaC and runtime is itself a Clause 8.1 (Operational planning) finding — and runtime queries are the only way to detect it.
Steampipe uses a Postgres-compatible SQL interface over plugins for AWS, Azure, GCP, GitHub, Microsoft Entra ID, Google Workspace, Okta, Slack, and dozens more.
### Key queries for ISO 27001 evidence
#### A.8.2 — Privileged access rights
```sql
-- Find IAM users with admin policies attached
SELECT u.name, p.policy_name
FROM aws_iam_user u
JOIN aws_iam_user_policy_attachment a ON a.user_arn = u.arn
JOIN aws_iam_policy p ON p.arn = a.policy_arn
WHERE p.policy_name = 'AdministratorAccess';
```
#### A.5.18 — Access rights review
```sql
-- Find IAM users who haven't logged in for 90+ days
SELECT name, password_last_used
FROM aws_iam_user
WHERE password_last_used < now() - interval '90 days'
OR password_last_used IS NULL;
```
#### A.8.5 — Secure authentication
```sql
-- Find IAM users without MFA
SELECT name, mfa_enabled
FROM aws_iam_user
WHERE mfa_enabled = false
AND password_enabled = true;
```
#### A.5.15 — Access control (Microsoft Entra ID)
```sql
-- Find guest users with privileged role assignments
SELECT u.display_name, r.role_definition_name
FROM azuread_user u
JOIN azuread_directory_role_assignment a ON a.principal_id = u.id
JOIN azuread_directory_role r ON r.id = a.role_definition_id
WHERE u.user_type = 'Guest';
```
#### A.8.4 — Source code access
```sql
-- Find GitHub repos without branch protection on main
SELECT full_name, default_branch
FROM github_my_repository r
LEFT JOIN github_branch_protection b ON b.repository_full_name = r.full_name
AND b.name = r.default_branch
WHERE b.repository_full_name IS NULL;
```
#### A.8.15 — Logging
```sql
-- Find AWS regions where CloudTrail is not enabled
SELECT region
FROM aws_region
WHERE region NOT IN (
SELECT home_region FROM aws_cloudtrail_trail WHERE is_multi_region_trail = true
);
```
### Continuous evidence generation
Steampipe queries can be wrapped in [Powerpipe](https://powerpipe.io/) dashboards/benchmarks for continuous compliance evidence. Schedule them daily or hourly to maintain "operational effectiveness" evidence required for SOC 2 Type II — which goes beyond ISO 27001's point-in-time stance but is often required in dual-audit scenarios.
---
## Unified output schema
The wrapper around all three tools should normalize findings into:
```json
{
"scan_id": "uuid",
"repository": "org/repo",
"commit_sha": "abc123",
"soa_version": "1.4",
"scanned_at": "2026-05-07T10:00:00Z",
"tools_used": ["checkov", "trivy", "steampipe"],
"findings": [
{
"finding_id": "F-001",
"tool": "checkov",
"tool_check_id": "CKV_AWS_19",
"severity": "high",
"title": "S3 bucket without server-side encryption",
"resource": "aws_s3_bucket.data",
"file": "infrastructure/storage.tf",
"line": 42,
"iso_27001_2022_controls": ["A.8.24", "A.8.10"],
"nist_800_53_r5_controls": ["SC-28"],
"cis_v8_1_controls": ["3.11"],
"soc_2_tsc": ["CC6.6"],
"soa_status": "included_implemented",
"suppression": null
}
],
"documentation_findings": [],
"verification_scope": {
"deterministic_in_repo": true,
"agentic_policy_review": true,
"out_of_repo_evidence_pointers": true,
"runtime_state_via_steampipe": true,
"behavioral_compliance": false,
"physical_validation": false
}
}
```
This is the artifact that gets attached to the PR for human review and that becomes audit evidence.
---
## Container packaging
Distribute the wrapper as a single container image with all three tools pre-installed. Example Dockerfile sketch:
```dockerfile
FROM python:3.12-slim
RUN pip install checkov
COPY --from=aquasec/trivy:latest /usr/local/bin/trivy /usr/local/bin/trivy
COPY --from=turbot/steampipe:latest /usr/local/bin/steampipe /usr/local/bin/steampipe
COPY wrapper/ /opt/iso27001-scanner/
ENTRYPOINT ["python", "/opt/iso27001-scanner/scan.py"]
```
The wrapper script orchestrates the three engines, normalizes outputs, applies SoA filtering, and produces the unified JSON report.
@@ -0,0 +1,247 @@
# Violation Patterns — Code, Configuration, Dependency
This file catalogs concrete violation patterns the scanner must detect. Patterns are organized by where they live in the repository: source code, infrastructure-as-code, or dependency manifests.
For each pattern: the violation, the controls violated, and the detection logic.
---
## Code-level violations
### SQL injection via string concatenation
**Controls violated**: A.8.28 (Secure coding), A.8.3 (Information access restriction).
**Pattern**:
```python
query = "SELECT * FROM users WHERE id = " + user_input
cursor.execute(query)
```
**Detection**: SAST rule (Semgrep, CodeQL) flagging string concatenation into SQL execution functions. Compare to parameterized form `cursor.execute("SELECT * FROM users WHERE id = %s", (user_input,))`.
### Command injection via shell concatenation
**Controls violated**: A.8.28.
**Pattern**:
```python
os.system("ping " + user_input)
subprocess.call(f"git clone {repo_url}", shell=True)
```
**Detection**: SAST rule for `os.system`, `subprocess.*` with `shell=True` and unsanitized inputs.
### Hardcoded secrets
**Controls violated**: A.8.24 (Use of cryptography), A.5.17 (Authentication information).
**Pattern**:
```python
AWS_SECRET_KEY = "AKIAIOSFODNN7EXAMPLE"
db_password = "p@ssw0rd123"
```
**Detection**: Entropy analysis + regex (Trivy secret scanner, gitleaks, truffleHog). Scan full Git history, not just current HEAD — secrets in old commits are still exposed.
### Weak cryptography
**Controls violated**: A.8.24.
**Patterns**:
```python
import hashlib
hashlib.md5(password.encode()) # Broken
hashlib.sha1(password.encode()) # Broken
```
```javascript
crypto.createHash('md5') // Broken
```
**Detection**: SAST rule for MD5, SHA-1, DES, RC4, ECB mode, or hashing without salt for password storage. Required alternatives: SHA-256+ for hashing, bcrypt/argon2/scrypt for passwords.
### Missing input validation on PII handlers
**Controls violated**: A.8.11 (Data masking), A.5.34 (Privacy and protection of PII).
**Pattern**: Functions that accept PII parameters and write them to logs, return them in error messages, or store them unencrypted.
**Detection**: Tag-based SAST — annotate PII-handling functions and verify masking/redaction is applied before output sinks (logs, errors, storage).
### Insecure deserialization
**Controls violated**: A.8.28.
**Patterns**:
```python
pickle.loads(user_input)
yaml.load(user_input) # Without SafeLoader
```
**Detection**: SAST rule for known-unsafe deserializers with untrusted input.
### Path traversal
**Controls violated**: A.8.3, A.8.28.
**Pattern**:
```python
open(os.path.join(base_dir, user_provided_filename))
```
**Detection**: SAST rule for filesystem operations using unsanitized user input. Required guard: realpath check that resolved path is within base_dir.
---
## Configuration-level violations (IaC)
### Public storage buckets
**Controls violated**: A.8.12 (DLP), A.8.3 (Access restriction), A.5.34.
**Patterns**:
```hcl
resource "aws_s3_bucket" "data" {
acl = "public-read"
}
```
```hcl
resource "aws_s3_bucket_public_access_block" "data" {
block_public_acls = false
block_public_policy = false
}
```
**Detection**: Checkov CKV_AWS_53, CKV_AWS_54, CKV_AWS_55, CKV_AWS_56.
### Wildcard IAM permissions
**Controls violated**: A.8.2 (Privileged access rights).
**Pattern**:
```json
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
```
**Detection**: Checkov CKV_AWS_1, CKV_AWS_46, CKV_AWS_49. JSON path query for any policy statement with `Action: *` AND `Resource: *`. Some narrow exceptions exist (e.g., `iam:GetUser` on self) but `*:*` is never justifiable.
### Unencrypted storage
**Controls violated**: A.8.24.
**Patterns**:
```hcl
resource "aws_ebs_volume" "data" {
encrypted = false # Or omitted (default false in older provider versions)
}
resource "aws_db_instance" "main" {
storage_encrypted = false
}
```
**Detection**: Checkov CKV_AWS_3 (EBS), CKV_AWS_16 (RDS), CKV_AWS_17 (RDS publicly accessible), CKV_AWS_19 (S3 server-side encryption).
### Database in public subnet
**Controls violated**: A.8.20 (Network security), A.8.22 (Segregation of networks).
**Pattern**: RDS / managed database resource attached to a subnet whose route table has a route to an Internet Gateway, plus a security group allowing 0.0.0.0/0 ingress on the database port.
**Detection**: Graph-based check (Checkov supports this via cross-resource attribute queries).
### Unrestricted security group ingress
**Controls violated**: A.8.20, A.8.22.
**Pattern**:
```hcl
resource "aws_security_group" "open" {
ingress {
from_port = 0
to_port = 65535
cidr_blocks = ["0.0.0.0/0"]
}
}
```
**Detection**: Checkov CKV_AWS_24 (port 22 / SSH from 0.0.0.0/0), CKV_AWS_25 (port 3389 / RDP), generic checks for all-port ingress from 0.0.0.0/0.
### Missing TLS enforcement
**Controls violated**: A.8.20, A.8.24.
**Patterns**:
```hcl
resource "aws_lb_listener" "http" {
protocol = "HTTP" # Should be HTTPS
port = 80
}
```
S3 buckets without bucket policy denying non-HTTPS requests.
**Detection**: Checkov CKV_AWS_2 (ALB listener HTTPS), CKV_AWS_103 (TLS 1.2+ for ALB), CKV_AWS_91 (ALB access logging).
### Privileged containers
**Controls violated**: A.8.18 (Use of privileged utility programs).
**Pattern**:
```yaml
spec:
containers:
- name: app
securityContext:
privileged: true
runAsUser: 0
capabilities:
add: ["SYS_ADMIN"]
```
**Detection**: Checkov CKV_K8S_16, CKV_K8S_20, CKV_K8S_22.
### Missing resource limits
**Controls violated**: A.8.6 (Capacity management).
**Pattern**: Kubernetes containers without `resources.limits.cpu` and `resources.limits.memory`.
**Detection**: Checkov CKV_K8S_10, CKV_K8S_11, CKV_K8S_12, CKV_K8S_13.
### Missing logging
**Controls violated**: A.8.15 (Logging).
**Pattern**: AWS resources without CloudTrail enabled, S3 buckets without access logging, VPCs without flow logs.
**Detection**: Checkov CKV_AWS_67 (CloudTrail multi-region), CKV_AWS_18 (S3 access logging), CKV_AWS_11 (VPC flow logs).
### Unprotected branch
**Controls violated**: A.8.4 (Access to source code), A.8.32 (Change management).
**Pattern**: GitHub `main` / `master` branch without protection rules requiring PR review, status checks, and signed commits.
**Detection**: Steampipe query against GitHub API, or scan of `.github/settings.yml` if probot/settings is used.
---
## Dependency-level violations
### Known CVEs in production dependencies
**Controls violated**: A.8.8 (Management of technical vulnerabilities), A.8.30 (Outsourced development).
**Detection**: Trivy / Snyk / Dependabot scanning `package-lock.json`, `requirements.txt`, `go.sum`, `Cargo.lock`, `pom.xml`, `Gemfile.lock`, `composer.lock`. Threshold: any CVSS ≥ 7.0 with available patch is blocking.
### Unpinned dependencies
**Controls violated**: A.8.8, A.8.32.
**Patterns**:
```
# requirements.txt
requests
django>=3.0
```
```json
// package.json
"lodash": "^4.0.0"
```
**Detection**: Manifest parsing for version specifiers using `>=`, `^`, `~`, or no version. Floating versions break reproducibility and audit trail.
### Missing lockfile
**Controls violated**: A.8.32 (Change management).
**Pattern**: `package.json` without `package-lock.json`, `requirements.txt` without `requirements.lock` or `Pipfile.lock`.
**Detection**: Filesystem check.
### Abandoned / unmaintained dependencies
**Controls violated**: A.8.8, A.8.30.
**Detection**: Cross-reference dependency list with deps.dev / libraries.io / OSV to detect packages with no commits in >24 months.
### License risk
**Controls violated**: A.5.32 (Intellectual property rights).
**Detection**: License scanner (FOSSA, Black Duck, license-checker) flagging GPL-3.0, AGPL-3.0, SSPL where they conflict with the repository's own licensing strategy.
### Typosquatting / dependency confusion
**Controls violated**: A.8.30, A.5.21 (ICT supply chain).
**Detection**: Known-bad-package lists, registry mirror verification, internal package namespace enforcement.
---
## Pipeline-level violations
### SAST not in pipeline
**Controls violated**: A.8.28, A.8.29.
**Detection**: Parse `.github/workflows/*.yml` and `.gitlab-ci.yml`. Verify a SAST step (Semgrep, CodeQL, SonarQube, Snyk Code) runs on every PR targeting protected branches.
### SCA not in pipeline
**Controls violated**: A.8.8.
**Detection**: As above, for SCA steps (Trivy, Snyk, Dependabot, OWASP Dependency-Check).
### IaC scanner not in pipeline
**Controls violated**: A.8.9, A.8.27.
**Detection**: As above, for IaC scanning (Checkov, tfsec, Terrascan).
### Secret scanning not in pipeline
**Controls violated**: A.8.24, A.5.17.
**Detection**: As above, for secret scanning (gitleaks, truffleHog, GitHub native secret scanning).
### No environment separation
**Controls violated**: A.8.31 (Separation of dev/test/prod).
**Pattern**: Single Terraform state file or single AWS account hosting both staging and production resources.
**Detection**: Workspace / state file inspection. Account ID checks across environment-specific variable files.
### Auto-merge bypassing review
**Controls violated**: A.8.4, A.8.32.
**Pattern**: GitHub auto-merge enabled on PRs without required reviewers, or CODEOWNERS not enforced.
**Detection**: Repository settings via Steampipe or GitHub API.
@@ -0,0 +1,140 @@
---
name: oss-license-compliance
description: Open source license compliance reference for repo scanning, SBOM generation, copyleft contamination, and CI/CD enforcement. Covers SPDX License List (JSON ingestion, expressions, matching), REUSE, Apache 2.0 NOTICE, OSADL Compatibility Matrix, FSF GPL/LGPL logic, AGPL §13 network-use, SSPL §13 service source code, BSL 1.1 competitive offering, license-change events (MongoDB, Elastic, Redis, HashiCorp), wrapping ScanCode and ORT (.ort.yml, rules.kts), SCANOSS/FossID snippet detection, agentic reasoning for ambiguous triggers, and mappings to NIST 800-53, CIS v8.1, ISO 27001:2022 Annex A, SOC 2 TSC, OpenChain ISO 5230. Trigger on OSS license scanning, SBOM, copyleft risk, AGPL/SSPL/BSL detection, license compatibility, dependency audits, M&A OSS diligence, REUSE/SPDX headers, NOTICE validation, ScanCode/ORT orchestration, "can we ship this with proprietary code", "what does AGPL mean for SaaS", "scan deps for copyleft", or mention of SPDX identifiers or open source license risk.
---
# Open Source License Compliance
This skill encodes the open source license compliance domain as it applies to source repositories, dependency trees, CI/CD pipelines, and the agentic auditing layer that sits on top of them. It is a compliance oracle for building or operating an automated OSS compliance scanner that runs at pull request time and on demand.
This skill answers four categories of question:
1. What does the license actually require, license by license, and which framework (SPDX, REUSE, OSADL, FSF) is the canonical source?
2. Which findings are deterministically verifiable from the repository alone, which require LLM reasoning over policy artifacts, and which are fundamentally out of repo?
3. How should a CI/CD scanner be architected, triggered, and scoped to honestly serve a license compliance program?
4. How do raw scan findings map into NIST 800-53, CIS v8.1, ISO 27001:2022, SOC 2 TSC, and OpenChain ISO/IEC 5230:2020 audit vernacular?
Use this skill instead of training data whenever working on OSS license scanning, SBOM enrichment, copyleft contamination evaluation, license-change event tracking, or framework-control mapping. Upstream catalogs (SPDX list, OSADL matrix, vendor license terms) update frequently; the structured guidance below tells you where to fetch the live data, not what the live data currently is.
## Authoritative sources
The skill draws from four upstream truth sources. Always pin against a version, never trust a mirror.
* **SPDX License List** — `spdx/license-list-XML` (governance, source XML) and `spdx/license-list-data` (machine-readable JSON, RDFa, HTML, plaintext distributions). Ingest `licenses.json` and `exceptions.json` from `license-list-data` releases (semantic versioning, `vX.Y` or `vX.Y.Z`). Pin to a specific release for auditability.
* **OSADL FOSS License Compatibility Matrix** — `matrix.json` (associative), `matrixseq.json` (indexed), `matrixseqexpl.json` (indexed with textual legal explanations). Ingest at scanner sync time, not at scan time.
* **FSF compatibility logic** — encoded in this skill (see `references/compatibility-and-copyleft.md`). Cross-reference against the FSF's published compatibility tables for GPL-family transitions.
* **REUSE Specification** — FSFE-maintained. The `reuse` linter is the canonical conformance tool; do not roll your own.
## When to consult which reference
Read the relevant reference file when the task touches that domain. Do not preload all of them.
| Task | Reference |
|------|-----------|
| SPDX identifiers, matching guidelines, license expressions, REUSE conformance, NOTICE file structure, attribution preservation | `references/spdx-and-detection.md` |
| License compatibility checks, copyleft contamination logic, AGPL Section 13, SSPL Section 13, BSL 1.1 competitive-offering evaluation, license-change events (MongoDB, Elastic, Redis, HashiCorp) | `references/compatibility-and-copyleft.md` |
| Wrapping ScanCode Toolkit, OSS Review Toolkit (ORT) `.ort.yml` and `rules.kts`, SCANOSS / FossID snippet detection, false-positive triage, honest limits of automation | `references/scanners-and-tooling.md` |
| Real-world violation patterns, hidden Gist licenses, transitive copyleft, Stack Overflow CC-SA viral effect, legal precedents (Orange/Entr'ouvert, Copilot class action) | `references/violation-patterns.md` |
| Mapping findings to NIST 800-53 Rev. 5, CIS v8.1, ISO/IEC 27001:2022 Annex A, SOC 2 TSC, OpenChain ISO/IEC 5230:2020 | `references/cross-framework-mapping.md` |
| Agentic prompts for ambiguous evaluations (BSL competitive-offering, AGPL network-use, dual-licensed choice resolution) | `references/agentic-prompts.md` |
## Verification taxonomy
Every license compliance check falls into one of three buckets. Be honest about which.
### 1. Deterministic from repository contents
Pure script logic. No reasoning required.
* Presence and exact path of `LICENSE`, `NOTICE`, `LICENSES/` directory, `.reuse/dep5`.
* SPDX identifier headers in source files (REUSE conformance via the `reuse` linter).
* Manifest parsing (`pom.xml`, `build.gradle`, `package.json`, `package-lock.json`, `go.mod`, `Cargo.toml`, `requirements.txt`, `Pipfile.lock`) cross-referenced against the local `licenses.json` snapshot.
* Version-to-license mapping for components that have changed license over time (e.g., MongoDB ≥ 4.0.3 → SSPL, Elasticsearch 7.11–7.16 → SSPL/Elastic, Redis ≥ 7.4 → RSALv2/SSPL, Terraform > 1.5.5 → BSL 1.1).
* OSADL matrix lookup for declared license pairs.
* `isOsiApproved` / `isFsfLibre` boolean gates from the SPDX JSON.
* SBOM generation in SPDX or CycloneDX format.
* Compiled rule evaluation via ORT `rules.kts`.
If a check can be expressed as "does file X contain string Y" or "does pair (A, B) appear in the matrix", it belongs here. Use ScanCode + ORT + the `reuse` linter and stop reasoning.
### 2. Agentic reasoning over policy artifacts
Required when the legal trigger depends on facts about the host application that no manifest exposes.
* **AGPL Section 13** — does the host expose modified AGPL code "remotely through a computer network"? Reading `Dockerfile`, `docker-compose.yml`, `kubernetes/`, deployment manifests, and `README.md` to determine whether the application is internal-only, air-gapped, or user-facing.
* **SSPL Section 13** — does the host offer the SSPL component "as a service to third parties"? The Service Source Code definition is sweeping (management, UI, APIs, automation, monitoring, backup, storage, hosting). Requires reading architecture docs.
* **BSL 1.1 competitive offering** — does the host constitute a product that "competes with HashiCorp's commercial offerings"? Pure business-logic question.
* **Dual-license selection** — when a dependency offers a choice (e.g., MPL-2.0 OR Apache-2.0), the project's outbound license and intended distribution must be reconciled with the choice. Encode the choice in `.ort.yml` once resolved so it does not require reasoning on every scan.
* **REUSE-style license choice** for files whose origin or intent is genuinely ambiguous.
The agent's reasoning must be logged to a tamper-evident audit trail (commit-pinned input, prompt, model, output). The decision becomes the verifiable artifact.
See `references/agentic-prompts.md` for prompt templates that produce structured JSON output suitable for ORT consumption.
### 3. Fundamentally out of repository
No script, no agent, no commit-bound artifact can resolve these. Generate evidence pointers (URLs, ticket references, document hashes) that link to external systems.
* **Commercial supplier indemnification** — vendor contracts representing OSS-clean deliverables and indemnifying OSS-related IP claims.
* **M&A and cyber insurance OSS warranties** — the SBOM and triaged conflict log are the input; the warranty document is the artifact.
* **Shadow SaaS / SaaS-to-SaaS OAuth** — IdP logs, SaaS Management Platform (Zylo, BetterCloud) data; not in the repo.
* **End-user-facing attribution UIs** — the LICENSE file is in the repo, but whether the deployed product surfaces it to users (Apache 2.0 §4(d), MIT attribution preservation) requires UI inspection.
Document the pointer, not the content. The compliance program is evidenced by the chain of pointers, not by duplicating external systems into the repo.
## Trigger and suppression signals
The scanner must run on the right diffs and stay quiet on the rest.
### Trigger on any of:
* Manifest changes — `pom.xml`, `build.gradle`, `package.json`, `package-lock.json`, `go.mod`, `go.sum`, `Cargo.toml`, `Cargo.lock`, `requirements.txt`, `Pipfile.lock`, `pyproject.toml`, `composer.json`, `Gemfile.lock`.
* Policy changes — `.ort.yml`, `.reuse/dep5`, `LICENSES/*`, `rules.kts`, `.whitesource`, scanner configuration. Forces a full repository re-baseline.
* Large uncommented code blocks introduced without a corresponding manifest change. Signals copy-paste; trigger snippet scanning.
* Dependency lockfile bumps even with no manifest change (transitive shifts).
* New `LICENSE`, `NOTICE`, or top-level legal file changes.
### Suppress / downgrade severity for:
* `/test`, `/tests`, `/__tests__`, `/spec`, `/mocks` — typically not distributed; copyleft distribution triggers do not apply. AGPL and SSPL network-use triggers may still apply if the test harness itself becomes a service.
* `.github/workflows/`, `.gitlab-ci.yml`, internal build tooling — not distributed to end users.
* Generated code directories explicitly listed in `.ort.yml` `excludes` (with a reasoned justification).
* Vendored documentation and example assets clearly marked as illustrative.
Suppression is configuration, not silence. Encode every suppression in `.ort.yml` with a path glob and a reason, so the audit trail explains why the scanner ignored it.
## Architecture pattern
The recommended architecture is a three-layer pipeline:
1. **Sync layer (scheduled, cached)** — Fetch SPDX `licenses.json`, OSADL matrix JSON, and any vendor advisory feeds. Pin versions in `.compliance/sources.lock`.
2. **PR-time scanner (deterministic)** — ScanCode (via Docker) for source-level detection; ORT analyzer + scanner + evaluator for dependency-tree evaluation; the `reuse` linter for header conformance; SCANOSS for snippet detection if license budget allows. Output structured JSON, fail the build on hard violations, post a PR comment summarizing soft findings.
3. **On-demand agentic auditor** — Invoked manually or on flagged dependencies. Reads the committed scan output plus repository policy artifacts (`README.md`, `architecture.md`, deployment manifests). Produces structured JSON with reasoning chain. Persists to an audit log keyed by commit SHA + prompt hash.
This separation is important: the deterministic layer must be fast and binary, the agentic layer must be slow and reasoned. Mixing them produces a scanner that is both flaky and slow.
## Output artifacts
Every scanner run produces, at minimum:
* **SBOM** in SPDX 2.3 or CycloneDX 1.5 (preferably both). The SBOM is the lingua franca for downstream consumers (M&A diligence, cyber insurance, OpenChain conformance).
* **Violations report** in JSON with a stable schema (eval ID, severity, license identifier, package coordinate, path, rule that fired, suppression status, evidence pointer).
* **Attribution bundle** — concatenated `LICENSE` and `NOTICE` content for every distributed dependency, suitable for shipping in product about-screens or `THIRD_PARTY_NOTICES.md`.
* **Audit trail entry** for every agentic decision, with commit SHA, scanned input hash, prompt, model identifier, and structured output.
The SBOM and the audit trail together are the evidence packet. Everything else is derived.
## Honest limits
State these limits explicitly to consumers of the skill, in PR comments and in audit-package documentation.
* **Dynamic linking and obfuscation** — SCA cannot see what is dynamically linked into a binary at runtime, nor can it identify obfuscated Java (ProGuard, R8) or stripped native binaries.
* **Reachability ≠ obligation** — A copyleft dependency reached only on a dead code path is still distributed and still triggers obligations. Do not let runtime SCA reachability filters quietly suppress legal risk.
* **Snippet false positives** — Boilerplate, autogenerated stubs, and standard algorithms produce constant noise. Confidence thresholds and persisted triage decisions in `.ort.yml` are the only way to keep alert fatigue manageable.
* **License-change events** — A dependency licensed permissively today may relicense tomorrow (Redis 2024, HashiCorp 2023, Elastic 2021/2024). Continuous monitoring of upstream license metadata is required; a one-time scan ages out fast.
* **Stack Overflow CC-SA** — Up to 33% of identified license conflicts in enterprise audits trace to Stack Overflow snippet pasting. Snippet scanning is the only line of defense.
* **The agent is non-deterministic** — Two runs of the same agentic prompt against the same input may differ. Pin model versions, log prompts, and treat the audit log as the artifact rather than the model output itself.
## Skill scope boundaries
This skill is a compliance oracle. It does not replace external legal counsel. When a finding has commercial consequences (M&A, large-scale copyleft contamination, willful violation exposure), the artifact this skill produces is the evidence packet for counsel; the legal judgment is theirs.
@@ -0,0 +1,200 @@
# Agentic Prompts for Ambiguous License Evaluation
The deterministic scanner identifies the presence of a high-risk license. It cannot determine whether the legal trigger of that license actually fires, because the trigger depends on facts about the host application that no manifest exposes. This reference provides prompt templates for the agentic auditor that resolves these cases.
## Operating principles
1. **The agent does not replace the scanner.** The scanner produces the deterministic finding ("MongoDB 6.0 is SSPL"); the agent answers the qualitative question ("does our deployment trigger §13").
2. **Output is structured.** Every prompt requires a JSON response so downstream tooling (ORT, the violations report, the audit log) can consume it.
3. **Reasoning is logged.** The prompt, model identifier, input file hashes, and full output are persisted to a tamper-evident audit trail keyed by commit SHA. The agent's reasoning becomes the verifiable artifact, not the model output alone.
4. **Conservatism is the default.** When evidence is ambiguous, the agent returns `is_compliant: false` with `confidence: low` and a request for human review. Optimistic defaults compound legal risk silently.
5. **Scope is bounded.** The agent reads only the files explicitly provided in the prompt. It does not browse the web, query external APIs, or infer facts not present in the inputs.
## Prompt template: AGPL §13 network-use evaluation
**When to invoke**: scanner has identified a direct or transitive AGPLv3 dependency in a repository that may deploy a network-facing service.
**Inputs to provide**:
* The dependency tree path showing how the AGPL component arrived.
* The repository's `Dockerfile`, `docker-compose.yml`, `kubernetes/` manifests, ingress configuration, deployment manifests.
* The repository's `README.md` and any `architecture.md` / `ARCHITECTURE.md`.
* The license text of the AGPL component.
**Prompt**:
> You are evaluating whether the AGPLv3 §13 ("Remote Network Interaction") trigger applies to a specific deployment of an AGPL-licensed component within the host repository whose files are attached.
>
> AGPLv3 §13 states that if the program is modified and the modified version is "interacted with remotely through a computer network", the operator must offer all interacting users the opportunity to receive the corresponding source code of the modified version.
>
> Evaluate three factual questions, in order:
>
> 1. **Modification**: Is the AGPL component modified by the host repository? Inspect any patches, monkey-patches, forks, configuration that overrides component behavior, or build steps that alter the component's source. Report yes / no / uncertain with evidence (file paths and excerpts).
> 2. **Network interaction**: Does the deployed application allow users to interact with the AGPL component remotely through a computer network? Inspect ingress configuration, exposed ports, public DNS records, deployment topology. Internal-only deployments behind a corporate VPN with no external user access do **not** trigger §13. SaaS deployments with external users **do** trigger §13. Air-gapped batch jobs **do not** trigger §13. Report yes / no / uncertain with evidence.
> 3. **User interaction with modified version**: Does the user's network interaction reach the modified portion of the AGPL component, or only unmodified portions? This is a narrower question than §13's literal text and conservative analysis treats any reachable modified code as triggering. Report yes / no / uncertain with evidence.
>
> Output a JSON object exactly matching this schema:
>
> ```json
> {
> "modification": {"answer": "yes|no|uncertain", "evidence": "..."},
> "network_interaction": {"answer": "yes|no|uncertain", "evidence": "..."},
> "modified_code_reachable": {"answer": "yes|no|uncertain", "evidence": "..."},
> "section_13_triggers": true|false,
> "confidence": "high|medium|low",
> "recommended_action": "...",
> "human_review_required": true|false
> }
> ```
>
> If any of the three sub-questions is `uncertain`, set `human_review_required` to `true` and `confidence` to `low`. The cost of false negatives in this domain (failing to detect a triggered §13) far exceeds the cost of false positives (escalating an untriggered case). Bias accordingly.
## Prompt template: SSPL §13 service-source-code evaluation
**When to invoke**: scanner has identified an SSPL-licensed component (MongoDB ≥ 4.0.3, Elasticsearch 7.11–7.16 / 7.16+ partial, Redis ≥ 7.4 dual SSPL, etc.) in a repository that may offer the component's functionality as a service.
**Inputs to provide**: same as AGPL template, plus the Service Source Code definition language from SSPL §13.
**Prompt**:
> You are evaluating whether the SSPL v1.0 §13 ("Offering the Program as a Service") trigger applies to a specific deployment of an SSPL-licensed component within the host repository whose files are attached.
>
> SSPL §13 states that if the functionality of the SSPL-licensed program is "made available to third parties as a service", the operator must release the **Service Source Code** under SSPL. The SSPL definition of Service Source Code is sweeping and includes the program plus management software, user interfaces, application program interfaces, automation software, monitoring software, backup software, storage software, and hosting software used to make the service available.
>
> Evaluate three factual questions, in order:
>
> 1. **Service offering to third parties**: Does the host repository deploy the SSPL component such that third parties (customers, partners, end users) can use the component's functionality? "Third parties" excludes employees of the operator and contractors working on the operator's behalf. Report yes / no / uncertain with evidence.
> 2. **Functionality offered**: What functionality of the SSPL component is exposed? Inspect API endpoints, exposed ports, application logic. Internal use of the component for the operator's own data processing, where the third-party-facing service does not surface the component's functionality, may not trigger §13. Direct exposure (e.g., offering a managed Redis API) does trigger §13. Report functionality details with evidence.
> 3. **Service Source Code scope**: If §13 triggers, identify which surrounding components fall within the Service Source Code definition (management, UI, APIs, automation, monitoring, backup, storage, hosting). This is the scope of source-disclosure obligation if compliance is the chosen path. Report the list with evidence.
>
> Output a JSON object exactly matching this schema:
>
> ```json
> {
> "service_to_third_parties": {"answer": "yes|no|uncertain", "evidence": "..."},
> "functionality_exposed": {"answer": "...", "evidence": "..."},
> "service_source_code_scope": ["..."],
> "section_13_triggers": true|false,
> "confidence": "high|medium|low",
> "recommended_action": "...",
> "human_review_required": true|false
> }
> ```
>
> SSPL §13 is broader and harder to comply with than AGPL §13. Where AGPL compliance generally requires only releasing the modified component's source, SSPL compliance can require releasing substantial proprietary infrastructure code. The recommended action in nearly all SaaS contexts is to remove the SSPL component or migrate to a permissively-licensed fork (Valkey for Redis, OpenSearch for Elasticsearch).
## Prompt template: BSL 1.1 competitive-offering evaluation
**When to invoke**: scanner has identified an HashiCorp BSL 1.1 component (Terraform > 1.5.5, Vault, Consul, Boundary, Waypoint, Nomad post-license-change) in the dependency tree.
**Inputs to provide**:
* Dependency tree path.
* The repository's `README.md`, `architecture.md`, product description / pitch deck if available.
* HashiCorp's current commercial-product list (provided as a static input, refreshed periodically).
* The BSL 1.1 license text.
**Prompt**:
> You are evaluating whether the host repository constitutes a "Competitive Offering" under HashiCorp's BSL 1.1 license, which would prohibit use of the BSL-licensed component in the host's commercial offering.
>
> BSL 1.1 defines a Competitive Offering as a product or service that is sold to third parties and:
> 1. Provides similar functionality to HashiCorp's commercial offerings, **or**
> 2. Embeds the BSL-licensed work such that the offering requires the BSL-licensed work to operate.
>
> Evaluate four factual questions, in order:
>
> 1. **Sold to third parties**: Is the host repository's product or service sold to third parties? Internal tools used only within the operating organization are not Competitive Offerings. Report yes / no / uncertain with evidence.
> 2. **Functional overlap with HashiCorp commercial offerings**: List HashiCorp's current commercial products (Terraform Cloud, HCP Vault, HCP Consul, etc., as supplied in the prompt input). Compare the host repository's functionality against each. Identify any meaningful overlap. Report findings with evidence.
> 3. **Embedded vs internal use**: Does the host repository embed the BSL-licensed component such that the host's offering requires the component to operate? "Embedded" means the host's customers receive or interact with the BSL component as part of the offering. "Internal use" means the host operator uses the BSL component to build or operate the offering, but customers do not interact with it. Report with evidence.
> 4. **Migration to fork available**: Is there a viable permissively-licensed fork (OpenTofu for Terraform, OpenBao for Vault) that the host could migrate to? Report with evidence.
>
> Output a JSON object exactly matching this schema:
>
> ```json
> {
> "sold_to_third_parties": {"answer": "yes|no|uncertain", "evidence": "..."},
> "functional_overlap": [{"hashicorp_product": "...", "overlap_description": "...", "evidence": "..."}],
> "embedded_use": {"answer": "yes|no|uncertain", "evidence": "..."},
> "fork_available": {"answer": "...", "evidence": "..."},
> "is_competitive_offering": true|false,
> "confidence": "high|medium|low",
> "recommended_action": "...",
> "human_review_required": true|false
> }
> ```
>
> Competitive Offering is a contested term; HashiCorp's enforcement posture is evolving. When the determination is non-obvious, set `human_review_required` to `true` and surface for legal review. Migration to a fork is the safest resolution for any host that is or might become a Competitive Offering.
## Prompt template: dual-license selection
**When to invoke**: scanner finds a dependency declared under an `OR` SPDX expression (e.g., `MIT OR Apache-2.0`, `MPL-2.0 OR Apache-2.0`, `GPL-2.0-or-later WITH Classpath-exception-2.0 OR EPL-2.0`), and `.ort.yml` does not yet contain a `license_choices` entry for it.
**Inputs to provide**:
* The dependency identifier and version.
* The full SPDX expression.
* The host repository's outbound license declaration.
* The host repository's distribution model (proprietary SaaS, distributed binary, source-available, etc.).
**Prompt**:
> You are selecting the optimal license choice for a dual-licensed (or multi-licensed) dependency that the host repository consumes. The chosen license becomes the license under which the host repository is using the dependency.
>
> Evaluate the following:
>
> 1. **Outbound compatibility**: For each license option in the SPDX expression, evaluate whether it is compatible with the host repository's outbound license. Use the OSADL FOSS License Compatibility Matrix logic and FSF compatibility rules.
> 2. **Distribution-model fit**: For each compatible option, evaluate whether the license terms are appropriate for the host's distribution model. SaaS hosts should prefer options that do not have network-use triggers (avoid AGPL where it is one of the choices). Distributed-binary hosts should prefer options that minimize attribution and source-disclosure burden.
> 3. **Recommendation**: Select the single best license. Provide reasoning.
>
> Output:
>
> ```json
> {
> "compatibility_evaluation": [
> {"license": "...", "compatible_with_outbound": true|false, "reason": "..."}
> ],
> "distribution_fit_evaluation": [
> {"license": "...", "fit_score": "high|medium|low", "reason": "..."}
> ],
> "selected_license": "...",
> "reasoning": "...",
> "confidence": "high|medium|low"
> }
> ```
>
> Once selected, the choice should be persisted in `.ort.yml` under `license_choices`, removing the need for re-evaluation on subsequent scans.
## Audit-trail schema
Every agentic invocation persists an audit-trail entry. Recommended schema:
```json
{
"decision_id": "uuid",
"timestamp": "ISO 8601",
"commit_sha": "...",
"trigger": "scanner-finding-id or manual",
"prompt_template": "agpl-section-13 | sspl-section-13 | bsl-competitive-offering | dual-license",
"input_file_hashes": {"path": "sha256", "...": "..."},
"scanner_finding": { /* the deterministic finding that triggered the agent */ },
"model_identifier": "claude-X.Y-YYYYMMDD",
"prompt": "the full prompt sent to the model",
"response": { /* the structured JSON response */ },
"human_review": {
"required": true|false,
"reviewer": "...",
"decision": "...",
"decision_timestamp": "ISO 8601"
}
}
```
The audit trail must be append-only and content-addressed (commit SHA + prompt-template hash + input-file-hash composite key). Replay against the same inputs produces a new entry, not a mutation of the old one. This preserves the historical record even if the model or prompt template is updated.
## When agentic review is overkill
Not every license finding deserves an agentic pass. Skip the agent when:
* The scanner finding is unambiguous (e.g., GPLv3 dependency in a clearly-internal CLI tool, AGPL dependency that has been allowlisted with documented exception).
* The repository has a standing `license_choices` curation covering the case.
* The deterministic policy in `rules.kts` produces a definitive verdict.
Reserve the agent for cases where the legal trigger genuinely depends on facts about the host application that the scanner cannot read. Overuse of the agent introduces latency, cost, and audit-log noise without compliance gain.
@@ -0,0 +1,190 @@
# License Compatibility, Copyleft, and High-Risk Triggers
This reference encodes the legal logic the scanner must apply when evaluating multi-license combinations and identifying high-risk licenses (AGPL, SSPL, BSL) that trigger consequences far beyond the standard copyleft model.
## OSADL FOSS License Compatibility Matrix
License compatibility is the framework that determines whether two licensed components can be combined and distributed together without contradiction. The Open Source Automation Development Lab (OSADL) maintains a machine-readable matrix that encodes pairwise compatibility for the major OSS licenses.
### Endpoints
Three JSON shapes from OSADL, choose by use case:
| Endpoint | Shape | Use |
|----------|-------|-----|
| `matrix.json` | Associative array `{outbound: {inbound: result}}` | Direct programmatic lookup by license pair. |
| `matrixseq.json` | Indexed array | Streaming or table generation. |
| `matrixseqexpl.json` | Indexed array with textual legal explanations | Human-readable PR comments and audit reports. |
Sync the matrix on a scheduled cadence (daily is sufficient) and pin the snapshot version in `.compliance/sources.lock`. Do not query OSADL on every PR scan.
### Result encoding
A pairwise lookup returns one of:
* **Compatible** — combination is permitted.
* **Incompatible** — combination violates one or both licenses; the matrix gives the contradiction reason in `matrixseqexpl.json`.
* **Conditional** — combination is permitted only if specific conditions are met (e.g., dynamic linking, system library exception). Requires reading the explanation field.
### Tooling
`flict` (FOSS License Compatibility Tool, Python) consumes the OSADL matrix directly and exposes:
* Compatibility verification across an entire dependency tree (input: SPDX SBOM).
* Candidate outbound license suggestion given a set of inbound licenses.
* Policy file evaluation — supply your organization's outbound license and a list of disallowed licenses; `flict` returns a verdict.
Wrap `flict` as a CI step rather than reimplementing matrix lookup logic in scanner code.
## FSF compatibility logic
The Free Software Foundation publishes the canonical compatibility tables for the GPL family. The scanner must encode the following:
### Permissive licenses
MIT, BSD-2-Clause, BSD-3-Clause, ISC, Apache 2.0 (with the patent-clause caveat below) are broadly compatible. Code under permissive licenses can be incorporated into copyleft-licensed projects, proprietary projects, and other permissively-licensed projects.
### GPL family
| Combination | Compatible? | Notes |
|-------------|-------------|-------|
| GPLv2-only + GPLv3 | **No** | Patent termination and indemnification differ. |
| GPLv2-or-later + GPLv3 | Yes | The "-or-later" allows promotion to GPLv3. |
| GPLv2-only + Apache 2.0 | **No** | Same patent-clause incompatibility. |
| GPLv3 + Apache 2.0 | Yes | GPLv3 was drafted to absorb Apache 2.0's patent clause. |
| GPLv3 + LGPLv2.1 | Yes | LGPLv2.1 is upgradable to GPLv3 via its `or-later` provision (when present). |
| GPLv3 + LGPLv3 | Yes | Designed to interoperate. |
| AGPLv3 + GPLv3 | Yes | AGPLv3 §13 explicitly allows linking with GPLv3. |
### LGPL nuance: linking model
The Lesser GPL is the only mainstream copyleft that allows proprietary code to use the LGPL component without the proprietary code becoming LGPL-licensed. The mechanism depends on linking:
* **Dynamic linking** (proprietary code calls LGPL library at runtime) — proprietary code remains proprietary; user must be able to relink against a modified LGPL library.
* **Static linking** (LGPL library compiled into proprietary binary) — proprietary code must allow the user to substitute a modified library, typically by shipping object files or statically linkable artifacts.
The scanner cannot determine link model from manifests alone. Surface the LGPL finding with a flag for human review of the build configuration, or use agentic reasoning over the `Makefile` / `CMakeLists.txt` / `Cargo.toml` link configuration.
### System library exception
GPLv2 and GPLv3 both contain a "system library" exception: a GPL-incompatible library that qualifies as a standard system component (libc, OS-shipped runtime libraries) can be linked with a GPL program without forcing source disclosure of that library. The scanner must not flag system-library combinations as violations; the dependency analyzer should classify these via known system-library lists.
## Copyleft contamination policy
If the scanner detects a strong copyleft license (GPL, AGPL, EUPL, OSL, CDDL with linking restrictions) in any dependency — direct or transitive — within a repository flagged for proprietary commercial distribution, the pipeline should:
1. Fail the build on the PR with a clear, specific error message naming the dependency, the license, and the contamination path.
2. Block the merge.
3. Generate a high-severity supply chain alert in the audit log.
4. Provide a remediation checklist: replace dependency, isolate as a separate process (microservice boundary), comply with the license, or seek a commercial alternative.
The contamination path is essential. A developer who imported a permissive package needs to see that the GPL dependency arrived via three transitive hops, not just that "something is GPL".
## AGPL Section 13: the network-use trigger
AGPL closes the "ASP loophole" left by traditional GPL. Standard GPL distribution triggers fire only on physical / binary distribution; SaaS providers historically argued that providing functionality over the network was not distribution. AGPL §13 contradicts this directly.
> "...if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network... an opportunity to receive the Corresponding Source of your version..."
Consequences if triggered:
* The full corresponding source of the modified AGPL component must be made available to every user who interacts with the running service over a network.
* "Corresponding source" includes scripts to control installation and modifications, per GPLv3 definition incorporated by reference.
* Internal proprietary backend code that has been combined with the AGPL component is potentially within the scope of "Corresponding Source" — this is the contamination risk.
### Detection
* Static: any AGPL-licensed dependency in a repository deploying a network-facing service. Trigger an immediate critical finding.
* Agentic (see `agentic-prompts.md`): determine whether the deployment topology actually exposes the AGPL component over a network. Air-gapped, internal-only, or batch-processing deployments may not trigger §13. The agent reads `Dockerfile`, deployment manifests, ingress configuration, and architecture documentation to make the call.
### Resolution paths
* Replace the AGPL dependency with a permissively-licensed equivalent.
* Comply: publish the corresponding source.
* Air-gap: isolate the AGPL component in a process / service that has no network exposure to external users (often impractical).
* Negotiate a commercial dual-license with the upstream rightsholder (where offered).
## SSPL Section 13: service source code
The Server Side Public License v1.0 (MongoDB, October 2018) is AGPL-Section-13-on-steroids. SSPL §13 mandates that if the program's functionality is "made available to third parties as a service", the deployer must release the **Service Source Code**.
### Service Source Code definition
The SSPL definition is sweeping. It includes not only the licensed program but also:
* Management software
* User interfaces
* Application program interfaces (APIs)
* Automation software
* Monitoring software
* Backup software
* Storage software
* Hosting software
In practice this can pull the entire orchestration and operations stack of a SaaS deployment into scope.
### OSI status
The OSI did not approve SSPL as an open source license. Debian and Red Hat declined to package SSPL-licensed software as free software. Treat SSPL as **source-available**, not open source, in all internal documentation.
### Scanner behavior
* Flag SSPL components as highly restricted.
* Block introduction into any SaaS-facing build pipeline pending legal review.
* Do not auto-suggest SSPL replacements without confirming the consumer's distribution model.
## Business Source License (BSL) 1.1
The Business Source License is a source-available license designed to permit broad use while reserving "competitive offering" exclusions for the licensor. HashiCorp adopted BSL 1.1 in August 2023 (Terraform > 1.5.5, Vault, Consul, etc.).
### Mechanics
* Source is publicly readable and auditable.
* Use in production is permitted **except** as a "Competitive Offering".
* After a "Change Date" (typically four years), the work converts to a designated "Change License" (typically MPL 2.0 or Apache 2.0) and the BSL restrictions lift.
### Competitive Offering definition
A product or service sold to third parties that:
1. Provides similar functionality to the licensor's commercial offerings, **or**
2. Embeds the BSL-licensed work such that the offering requires it to operate.
### Why this is a hard call
Determining "competitive offering" requires evaluating:
* What the licensor sells commercially today.
* Whether the host application's functionality overlaps.
* Whether the BSL component is embedded vs. used as an internal tool.
* Whether the host application is sold to third parties at all.
This is not deterministic. The scanner detects the BSL component (deterministic); an agent evaluates the competitive-offering question (see `agentic-prompts.md` for the prompt template).
### Forks
Significant license-change events have spawned forks designed to preserve permissive licensing:
* **OpenTofu** — fork of Terraform, MPL 2.0, hosted under the Linux Foundation.
* **OpenBao** — fork of Vault, MPL 2.0.
Where a fork is viable and feature-complete, the cleanest resolution to a BSL finding is migration to the fork.
## License-change event registry
The scanner must encode the following historical license-change events. A dependency upgrade across one of these boundaries is automatically a high-severity finding.
| Project | Original | New | Change Date | Driver |
|---------|----------|-----|-------------|--------|
| MongoDB | AGPL | SSPL 1.0 | October 2018 | Block managed-service competitors. |
| Elasticsearch / Kibana | Apache 2.0 | Dual SSPL / Elastic License 2.0 | January 2021 | Block AWS Elasticsearch Service. |
| Elastic | SSPL/ELv2 | Added AGPLv3 (triple license) | August 2024 | Partial reversal; community pressure. |
| HashiCorp (Terraform, Vault, Consul, etc.) | MPL 2.0 | BSL 1.1 | August 2023 | Block competitive offerings; spawned OpenTofu, OpenBao. |
| Redis | BSD 3-Clause | Dual RSALv2 / SSPL 1.0 | March 2024 | Block managed Redis competitors; spawned Valkey (Linux Foundation). |
| Redis | RSALv2/SSPL | Added AGPLv3 | May 2025 | Partial reversal; ongoing fork dynamics. |
This registry ages. Always cross-check against the project's current license file at the version the scanner observes. A version-specific lookup is mandatory: MongoDB 3.6 is AGPL; MongoDB 4.0.3+ is SSPL. Treating MongoDB as a single license-bearing entity is wrong.
## Stack Overflow and CC-SA viral effect
Public Stack Overflow contributions are licensed CC BY-SA. Code copied from Stack Overflow into a proprietary codebase carries the share-alike obligation, which acts as a copyleft contamination vector. Up to a third of identified license conflicts in enterprise audits originate from Stack Overflow snippets. Snippet-level scanning (see `scanners-and-tooling.md`) is the only practical defense; relying on developer self-reporting is insufficient.
@@ -0,0 +1,102 @@
# Cross-Framework Mapping
OSS license compliance findings are inputs to multiple security and compliance frameworks. The scanner output (SBOM, violations report, audit trail) must speak the vernacular of each framework's auditors. This reference enumerates the mappings.
## NIST 800-53 Rev. 5
National Institute of Standards and Technology Special Publication 800-53 Revision 5 — security and privacy controls for federal information systems and supporting commercial infrastructure (FedRAMP, CMMC).
| Control | Name | OSS compliance mapping | Evidence artifact |
|---------|------|------------------------|-------------------|
| **CM-8** | System Component Inventory | The SBOM is the inventory of OSS components. Every build emits a versioned SPDX or CycloneDX SBOM, archived. | `sbom.spdx.json`, `sbom.cdx.json`, retained per release. |
| **CM-8(2)** | Automated Maintenance | The CI/CD-integrated scanner regenerates the SBOM on every merge to main, eliminating drift between deployed components and declared inventory. | CI pipeline logs showing SBOM generation step succeeded for every merged PR. |
| **CM-8(3)** | Automated Unauthorized Component Detection | The deny-list policy in `rules.kts` blocks unauthorized licenses (typically AGPL, SSPL in proprietary contexts). | Policy file + CI pipeline failure logs for blocked PRs. |
| **SA-8** | Security Engineering Principles | The compliance program is itself an engineering principle: deterministic checks at PR time, agentic review of ambiguous cases, audit trail for non-deterministic decisions. | Compliance program documentation. |
| **SA-15** | Development Process, Standards, and Tools | The CI/CD scanner enforces approved licenses and toolchain configuration as a deterministic gate. | Pipeline configuration showing the scanner is mandatory and not bypassable. |
| **SA-15(8)** | Reuse of Threat / Vulnerability Information | Scanner integrates with vulnerability advisories (Dependabot, OSV, GitHub Advisory) so license findings and CVE findings share the same dependency-tree resolver. | Combined SCA + license report. |
| **SI-12** | Information Management and Retention | License audit trails, NOTICE attribution files, and agentic decision logs are retained per organizational retention policy. | Retention policy documenting compliance artifact lifecycle. |
| **SI-12(1)** | Limit Personally Identifiable Information Elements | When telemetry is sent to external SCA or LLM services, scrub PII and proprietary logic. Document what leaves the boundary. | Data-flow diagram for scanner telemetry. |
| **SR-3** | Supply Chain Controls and Processes | The scanner is the implementation of supply chain controls for OSS components. | Supply chain risk management plan referencing the scanner. |
| **SR-4** | Provenance | SBOM with package coordinates, version pins, and source URLs documents OSS provenance. | SBOM. |
| **SR-11** | Component Authenticity | Verify package integrity (checksums, signatures) at scanner ingestion. | Lockfile + checksum verification step in CI. |
## CIS Critical Security Controls v8.1
Center for Internet Security Critical Security Controls v8.1 — prioritized, actionable safeguards.
| Safeguard | Name | OSS compliance mapping | Evidence artifact |
|-----------|------|------------------------|-------------------|
| **2.1** | Establish and Maintain a Software Inventory | SBOM covers every OSS component, including transitive dependencies and snippet-detected fragments. | SBOM, periodic snippet-scan results. |
| **2.2** | Ensure Authorized Software is Currently Supported | Scanner flags OSS components that are deprecated, unmaintained, or running unsupported versions. | SCA report with end-of-life flags. |
| **2.3** | Address Unauthorized Software | Deny-list licenses in `rules.kts`; block PRs that introduce them. | Pipeline failure log + denied license catalog. |
| **2.5** | Allowlist Authorized Software | Curated `licenseClassifications.yml` defining acceptable licenses per distribution model. | License-classification configuration. |
| **2.6** | Allowlist Authorized Libraries | The combined ORT analyzer + curation file allowlists specific package coordinates per repository. | `.ort.yml` curations + per-repo policy. |
| **3.1** | Establish and Maintain a Data Management Process | Audit-trail retention covers OSS compliance artifacts. | Retention policy. |
| **16.4** | Establish and Manage an Inventory of Third-Party Software Components | Same as CM-8 / 2.1: the SBOM. | SBOM. |
| **16.5** | Use Up-to-Date and Trusted Third-Party Software Components | Scanner integrates with vulnerability feeds; license-and-CVE findings share dependency resolution. | Combined report. |
## ISO/IEC 27001:2022 Annex A
The 2022 revision of ISO/IEC 27001 restructured Annex A around four control themes: Organizational (5), People (6), Physical (7), Technological (8). The relevant controls for OSS license compliance:
| Control | Name | OSS compliance mapping | Evidence artifact |
|---------|------|------------------------|-------------------|
| **5.19** | Information Security in Supplier Relationships | OSS upstreams are suppliers; the policy treats them as such. | Supplier policy referencing OSS upstreams; SBOM. |
| **5.20** | Addressing Information Security Within Supplier Agreements | Outsourced development contracts require flow-down of OSS scanning to subcontracted code. | Contract template + flow-down clause. |
| **5.21** | Managing Information Security in the ICT Supply Chain | SBOM, scanner audit trail, license-change event monitoring. | SBOM + monitoring procedure. |
| **5.22** | Monitoring, Review and Change Management of Supplier Services | Scanner reruns on dependency updates; license-change events trigger review. | Continuous monitoring procedure. |
| **5.31** | Legal, Statutory, Regulatory and Contractual Requirements | License obligations are contractual requirements; the program documents how they are met. | Statement of Applicability covering OSS license obligations. |
| **5.33** | Protection of Records | OSS audit-trail records (scan outputs, agentic decisions) are retained as evidence. | Retention procedure. |
| **8.4** | Access to Source Code | Where OSS license obligations require source disclosure (GPL, AGPL, MPL), the controls allow it without compromising proprietary boundaries. | Source-disclosure procedure. |
| **8.25** | Secure Development Lifecycle | The PR-time scanner is part of the SDLC. | SDLC documentation. |
| **8.28** | Secure Coding | Scanner blocks introduction of unapproved licenses and snippets. | Pipeline configuration. |
| **8.30** | Outsourced Development | Code from agencies and contractors must be scanned before acceptance. The scanner is the acceptance gate. | Acceptance procedure + scan logs from contractor deliverables. |
| **8.32** | Change Management | Every PR is scanned; the scan is part of the change-control evidence chain. | PR-scan logs. |
## SOC 2 Trust Services Criteria
AICPA TSP Section 100 (2017, revised 2022). Relevant Common Criteria for OSS license compliance:
| Criterion | Name | OSS compliance mapping | Evidence artifact |
|-----------|------|------------------------|-------------------|
| **CC2.1** | Information Quality | The SBOM is high-quality information about software composition. | SBOM with version pins and source provenance. |
| **CC3.2** | Risk Assessment | License risk (copyleft contamination, license-change events) is part of the risk assessment. | Risk register entries for OSS license risk. |
| **CC3.4** | Risk Management Process Updates | The scanner is updated for license-change events (Redis 2024, Elastic 2024, etc.). | Change log of `licenses.json` snapshot updates. |
| **CC4.1** | Continuous Monitoring | Scanner runs on every PR + on a nightly cadence + on release. | CI pipeline configuration + scheduled-job logs. |
| **CC7.1** | Change Identification | Lockfile changes trigger scans; license-change events are detected. | Trigger configuration + alert log. |
| **CC8.1** | Change Management | Every PR is reviewed against deterministic and (where applicable) agentic license policy before merge. | PR scan results stored per merge commit. |
| **CC9.1** | Risk Mitigation Plans | Detection of high-risk licenses (SSPL, AGPL in SaaS contexts) triggers documented mitigation. | Mitigation playbook. |
| **CC9.2** | Vendor and Business Partner Management | OSS upstreams are managed as the third-party-risk-managed entities they are. License changes are vendor change events. | Vendor-management register including OSS upstreams. |
## OpenChain ISO/IEC 5230:2020
ISO/IEC 5230 is the international standard for OSS license compliance program quality. The standard is short and program-focused; the scanner is the operational arm of conformance.
Conformance requires:
| Requirement | Mapping |
|-------------|---------|
| **3.1** Written program documentation | Compliance program document referencing this skill, the scanner, and the agentic auditor. |
| **3.2** Program scope | Defined: every repository in scope, every PR scanned, every release SBOM-published. |
| **3.3** Identified personnel and roles | Designated compliance owner, legal escalation path, scanner-tooling owner. |
| **3.4** Compliance training | Onboarding includes the scanner, `.ort.yml` curation, and high-risk license recognition. |
| **3.5** Bill of Materials | SBOM in SPDX or CycloneDX, generated per release, archived. |
| **3.6** Compliance artifact procedure | Documented: SBOM generation, NOTICE bundle assembly, attribution distribution. |
| **3.7** External open source request handling | Procedure for receiving and responding to source-disclosure requests (GPL, AGPL, MPL recipients). |
| **3.8** External community contribution policy | Outbound-contribution policy (this is adjacent to but distinct from inbound license compliance). |
| **3.9** Internal compliance program review | Periodic internal audit of program effectiveness. |
OpenChain conformance is an attestation, not a certification, but it is the de facto contract-level standard for OSS compliance maturity. Major customers and acquirers increasingly request OpenChain conformance attestation in supplier qualification.
## Cross-mapping at a glance
For a single OSS compliance finding, the mapping into framework vernacular:
* **Detected unapproved AGPL dependency in proprietary SaaS repository, blocked at PR time, audit trail retained.**
* NIST 800-53: CM-8(3) (automated unauthorized component detection), SA-15 (development process tools), SR-3 (supply chain controls).
* CIS v8.1: 2.3 (address unauthorized software), 2.6 (allowlist authorized libraries).
* ISO 27001:2022 Annex A: 5.21 (ICT supply chain), 8.28 (secure coding), 8.32 (change management).
* SOC 2 TSC: CC8.1 (change management), CC9.1 (risk mitigation), CC9.2 (vendor management).
* OpenChain ISO/IEC 5230: 3.5 (BOM), 3.6 (compliance artifact procedure).
This is the kind of multi-framework crosswalk that a single PR-time scan can produce, given the right reporting layer. Build the reporter to emit the framework annotations alongside the raw violation, and a single scanner run feeds every audit downstream.
@@ -0,0 +1,155 @@
# Scanners and Tooling: ScanCode, ORT, SCANOSS, FossID
Building a scanner from scratch is the wrong default. Mature OSS scanners have invested years in license matching corpora, regression test suites, and ecosystem-specific package manager integration. The skill author's job is orchestration, policy, and reporting — not reimplementing matching.
## ScanCode Toolkit
Reference implementation for license, copyright, and dependency metadata extraction. Maintained by AboutCode (Nexus B) under Apache 2.0.
### Why it is the baseline
* Full license-text diff matching, not approximate string matching. Detects licenses even when copyright placeholders, formatting, or whitespace differ.
* Tested with 30,000+ regression cases against the real-world distribution of license-text variants.
* Wide ecosystem coverage: Maven, npm, PyPI, Go modules, RubyGems, Cargo, Composer, NuGet, package-lock files, OS package metadata, archive contents.
* Outputs structured JSON suitable for downstream programmatic policy evaluation.
### Container-based CI integration
The official Docker image (`ghcr.io/aboutcode-org/scancode.io:latest`) is the most reliable invocation path; it pins the matcher version and avoids host Python environment drift.
```bash
docker run --rm \
-v "$(pwd)":/codedrop \
ghcr.io/aboutcode-org/scancode.io:latest \
run scan_codebase /codedrop \
> scancode_results.json
```
Cache the Docker layer in CI (`actions/cache`, GitLab `cache:`, etc.) — the image is large and uncached pulls add minutes per run.
### Output structure
ScanCode JSON contains `files[]` with per-file `licenses[]`, `copyrights[]`, and `package_data[]`. The downstream scanner consumes this as input — it should not re-derive license findings from raw source.
For SBOM generation, prefer `scancode-toolkit`'s `--spdx-tv` or `--spdx-rdf` outputs for SPDX, or chain through `cyclonedx-cli` for CycloneDX.
## OSS Review Toolkit (ORT)
ORT is the orchestration layer the skill should build on. It composes multiple scanners (ScanCode, Fossology, others) and adds policy evaluation, reporting, and SBOM generation.
### Pipeline architecture
ORT decomposes the workflow into discrete tools, each consuming the previous tool's JSON output:
1. **Analyzer** — resolves dependency trees from package manifests. Ecosystem-aware (Maven, npm, Cargo, etc.). Produces an `analyzer-result.json`.
2. **Scanner** — fetches sources and runs configured per-package scanners (ScanCode, Fossology). Produces `scan-result.yml`.
3. **Advisor** (optional) — queries security advisory databases for known vulnerabilities.
4. **Evaluator** — applies user-defined policy rules in Kotlin script (`rules.kts`) against the combined analyzer + scanner data.
5. **Reporter** — produces SBOMs (SPDX, CycloneDX), notice files, HTML reports, and developer-friendly summaries.
This decomposition lets the CI pipeline cache intermediate outputs, rerun only the policy layer when `rules.kts` changes, and ship the same reports to multiple downstream audiences.
### `.ort.yml` repository configuration
Per-repository configuration sits in `.ort.yml` at the repository root. Key sections:
* **`excludes.paths`** — path globs to exclude from analysis (vendored examples, generated code). Each exclude has a `reason` field; never silently exclude.
* **`excludes.scopes`** — exclude scopes like `test`, `devDependencies`. Use carefully; license obligations may apply to artifacts produced from devDependency tooling.
* **`curations`** — corrections to upstream metadata (a known-bad declared license overridden with the actual license).
* **`license_choices`** — for dual-licensed dependencies, record the choice made (e.g., for `MIT OR Apache-2.0`, the project chose `Apache-2.0`).
* **`resolutions`** — triaged decisions on findings the scanner produced; mark a snippet match as "false positive, boilerplate" with reason.
`.ort.yml` is the durable artifact of compliance triage. Treat it as code: review changes, require justification, never delete entries silently.
### `rules.kts` policy
ORT evaluates compliance via Kotlin script. Skill authors should ship a baseline `rules.kts` covering:
* Banned licenses (deny list, e.g., AGPL in proprietary distribution context).
* Required license choices (every dual-licensed dep must have a `license_choices` entry).
* Required attribution (every Apache 2.0 dep must have a NOTICE entry).
* Allowed license combinations against outbound license.
* Severity grading: `ERROR` blocks merge, `WARNING` produces PR comment.
Kotlin script is more powerful than YAML rules for this purpose — it can compose conditions, traverse the dependency graph, and emit structured violation objects. Resist the urge to reinvent it in a less expressive language.
### CLI invocation
```bash
ort analyze -i . -o ort/analyzer
ort scan -i ort/analyzer/analyzer-result.yml -o ort/scanner
ort evaluate \
-i ort/scanner/scan-result.yml \
--rules-file rules.kts \
--license-classifications-file license-classifications.yml \
--package-curations-file curations.yml \
-o ort/evaluator
ort report \
-i ort/evaluator/evaluation-result.yml \
-f WebApp,SpdxDocument,CycloneDx,NoticeTemplate \
-o ort/reporter
```
Pipeline steps cache cleanly: re-evaluating policy on a stable scan result is seconds; re-scanning is minutes.
## Snippet detection: SCANOSS, FossID
Manifest scanning misses copy-pasted code. Snippet detection compares source fingerprints against a corpus of known OSS files.
### SCANOSS
Open-source SCA engine with snippet matching. Fingerprint database covers hundreds of millions of files.
* Detection threshold: configurable, typically 6-line minimum match window.
* Resilient to reformatting, variable renaming, comment changes; sensitive to logic alteration.
* Correlates fingerprints to licenses, copyright holders, and (for vulnerable code) specific CVEs.
* Persists triage decisions via ORT integration: `.ort.yml` `snippet_choices` entries record "snippet match X is boilerplate, ignore" with audit-trail reason.
SCANOSS can be run as a self-hosted service or against the public API. Self-hosting eliminates source-code-leaving-premises concerns for proprietary repositories.
### FossID
Commercial SCA with snippet matching, broader corpus, and enterprise workflow integration. Use when:
* Snippet-detection corpus needs to exceed SCANOSS coverage.
* Compliance program requires a vendor SLA on detection accuracy.
* Integration with enterprise GRC platforms is required.
The skill remains the same; only the scanner selection changes. The orchestration / policy / reporting layers built on ORT can consume SCANOSS or FossID output equivalently if the integration layer normalizes findings.
## False positives and alert fatigue
Snippet scanners flag boilerplate, autogenerated stubs, and standard algorithms as matches. Without management, developer trust in the scanner collapses within weeks.
### Mitigation patterns
1. **Confidence thresholds** — most scanners expose a confidence score; require ≥ 80% match before raising a finding to a blocking severity.
2. **Persisted triage** — `.ort.yml` `snippet_choices` (or equivalent) records every triaged decision. The scanner respects the decision on subsequent runs.
3. **Boilerplate corpus** — maintain an internal allow list of known-boilerplate snippet hashes (autogenerated API stubs, common algorithm implementations, vendor SDKs).
4. **PR-comment severity tiering** — block on hard violations only; surface soft findings as informational PR comments without failing the build.
5. **Periodic full audits** — rerun the scanner with low thresholds quarterly, off the critical path. Triage findings into `.ort.yml` rather than at PR time.
The goal is not zero alerts. The goal is that every alert that reaches a developer represents a decision worth making.
## Honest limits of automated detection
State these in PR comments and audit-package documentation:
* **Dynamic linking** — runtime-linked libraries (system libraries, OS-provided runtimes) are invisible to source-level scanning. If the deployment payload includes an OS image, the OS layer must be scanned separately.
* **Obfuscation** — Java code processed by ProGuard or R8 strips function signatures, dead code, and class names. Snippet matching against obfuscated artifacts is unreliable. Scan pre-obfuscation source.
* **Reachability ≠ legal obligation** — runtime SCA tools can suppress findings on unreachable code paths. From a legal perspective, distribution of the code triggers obligations regardless of reachability. Do not allow a security-oriented reachability filter to suppress license findings.
* **Native binaries without symbols** — stripped C/C++/Rust binaries are largely opaque. Scan source, not stripped artifacts.
* **Licensed-by-reference** — a `README.md` "this is MIT" with no license text is detectable but not actionable; the scanner should flag for human resolution.
* **License changes mid-version-range** — a dependency that flipped licenses across versions requires lockfile-pinned version-aware scanning. Bare manifest scans can miss this.
## When to run which scanner
| Stage | Scanner | Cost | Catches |
|-------|---------|------|---------|
| Pre-commit / IDE | `reuse` linter | Negligible | Missing SPDX headers in new files. |
| PR-time fast | ORT analyzer + curated `rules.kts` | Seconds–low minutes | Manifest-level new dependencies, banned licenses. |
| PR-time deep | ORT analyzer + ScanCode scanner + evaluator | Minutes | License findings in actual fetched source, NOTICE compliance, attribution gaps. |
| Periodic / nightly | ORT + SCANOSS snippet | Tens of minutes | Copy-pasted snippets, transitive shifts. |
| Pre-release | Full ORT + SCANOSS + manual triage | Hours | Final SBOM, attribution bundle, audit packet. |
Tier the scanner so PR-time stays fast; reserve expensive analysis for nightly and release gates. The PR-time scanner that takes 20 minutes will be turned off; the one that takes 90 seconds becomes infrastructure.
@@ -0,0 +1,123 @@
# SPDX, REUSE, NOTICE: Detection and Attribution Reference
The deterministic detection layer of any OSS compliance scanner rests on the SPDX License List and adjacent attribution conventions. This reference defines the canonical inputs, the matching rules, and the structural checks that scripts must perform.
## SPDX License List framework
### Repository topology
Two repositories under the SPDX organization, with distinct roles:
* **`spdx/license-list-XML`** — the governance and authoring repository. Contains the source XML schema for every license and exception. Maintained by the SPDX Legal Team. Do **not** parse this directly from a scanner; the XML is structured for human curation, not machine evaluation.
* **`spdx/license-list-data`** — the downstream distribution repository. Compiled from `license-list-XML` into stable machine-readable formats: JSON, RDFa, HTML, plaintext. This is what the scanner ingests.
Both repositories release under semantic versioning tags (`vX.Y` or `vX.Y.Z`). Pin to a specific release in `.compliance/sources.lock` so two runs of the scanner against the same commit always evaluate against the same license catalog.
### JSON ingestion
Pull two arrays from `license-list-data`:
* `licenses.json` — every active and deprecated license entry.
* `exceptions.json` — every license exception (e.g., `Classpath-exception-2.0`, `LLVM-exception`, `Autoconf-exception-2.0`).
Per-entry fields the scanner must consume:
| Field | Purpose |
|-------|---------|
| `licenseId` | Standardized short identifier. ASCII, letters / digits / `.` / `-` only. The matching key. |
| `name` | Formal legal title, normalized (version abbreviations stripped from sort name). |
| `licenseText` / `standardLicenseTemplate` | Full prose; the diff target for full-text matching. |
| `seeAlso` | Canonical permanent URLs. |
| `isOsiApproved` | Boolean. OSI-approved → safe baseline for "open source" declarations. |
| `isFsfLibre` | Boolean. FSF-recognized free software license. |
| `isDeprecatedLicenseId` | Boolean. Deprecated identifiers still resolve but should be flagged for migration. |
Boolean gates compose into deterministic policy: e.g., fail the PR if any newly introduced direct dependency has `isOsiApproved == false` and is not on an explicit allow list.
## SPDX matching guidelines
License texts in the wild differ from canonical text by typography, formatting, copyright placeholders, and trivial wording variants. The SPDX Matching Guidelines define what differences are tolerable.
### Markup attributes
The XML schema marks regions of license text with attributes that the matcher must respect:
* **Omittable text** — visually styled blue in HTML; tagged via `altParagraphType` in the XML. Can be absent from a candidate without disqualifying the match. Common for optional preambles or address blocks.
* **Replaceable text** — visually styled red in HTML; represents fields like copyright holder name, year, project title. The matcher must capture these via wildcard / placeholder logic, not literal matching.
### Equivalent words
`equivalentwords.txt` in the SPDX repository catalogs spelling variants of legally significant terms (British vs American English: `licence`/`license`, `merchantability` variants, etc.). The matcher must treat listed variants as equivalent.
### Implementation
Do not write the matcher from scratch. Use ScanCode Toolkit, which embeds the full SPDX matching logic with 30,000+ regression tests. Scanner-author energy is better spent on the orchestration and policy layers.
## SPDX License Expressions
A single component may declare a compound license. The expression syntax must be parsed correctly.
### Operators
* `OR` — disjunction. `(MIT OR Apache-2.0)` means the consumer chooses one. The scanner must record the **chosen** license; an unresolved `OR` is a finding requiring agentic resolution or `.ort.yml` curation.
* `AND` — conjunction. All terms apply simultaneously.
* `WITH` — appends a standardized exception. `GPL-2.0-only WITH Classpath-exception-2.0` means GPLv2 with the linking exception. Look up the exception in `exceptions.json`.
* `+` — "or any later version". `GPL-2.0+` means GPLv2 or any later GPL. Operationally this is dangerous: future GPL versions cannot be evaluated for compatibility today, so policy should treat `+` as a flag for review.
### Suffixed identifiers
The SPDX list distinguishes `GPL-2.0-only` from `GPL-2.0-or-later`, `LGPL-3.0-only` from `LGPL-3.0-or-later`, etc. Older expressions (`GPL-2.0`) are deprecated; the scanner should normalize to the explicit `-only` / `-or-later` form and warn on the legacy bare identifier.
## Apache 2.0 NOTICE requirements
Apache 2.0 §4(d) is the most commonly violated attribution clause in commercial distributions. The scanner must enforce it explicitly.
### Required artifacts
For any distribution that includes Apache 2.0 components:
1. The Apache 2.0 license text in a `LICENSE` file at the top of the distribution.
2. A `NOTICE` file containing every upstream attribution notice, **carried forward unchanged** even if the consuming project modifies the underlying source. Modification is allowed; stripping is not.
3. The `NOTICE` file must contain only legally required notices. Marketing copy belongs elsewhere.
### What does not count
Git commit messages, commit timestamps, and authorship metadata are **not** attribution under §4(c). The legal artifact is the file content, not the version control history. A scanner that says "the author is in the commit log" is wrong.
### Scanner check
For every Apache 2.0 dependency identified by the analyzer, the scanner should:
1. Fetch the upstream `NOTICE` content (from the resolved package source, not from a third-party mirror).
2. Verify the local `NOTICE` file contains those lines. Truncated or stripped NOTICE content is a finding.
3. If the project modifies the dependency, verify the modification statement required by §4(b) is present.
## REUSE specification
The REUSE specification (FSFE) makes file-level license attribution machine-readable, eliminating ambiguity from the matcher entirely. A REUSE-compliant repository is the highest-confidence target for automated scanning.
### Conformance conditions
The `reuse` linter checks three things:
1. **Bulk metadata** — `REUSE.toml` (current spec) or `.reuse/dep5` (legacy DEP-5 syntax) for directories where per-file headers are impractical (assets, generated files).
2. **File-level identifiers** — every source file carries a header comment with `SPDX-FileCopyrightText:` and `SPDX-License-Identifier:` lines. Format depends on the file's comment syntax.
3. **License texts** — `LICENSES/` directory at the repository root containing the unaltered text of every license referenced anywhere in the codebase.
### Enforcement integration
Run the `reuse` linter in CI as a separate step from the SCA scan. On non-conformance, the scanner can auto-generate a PR comment with the missing SPDX header lines for each file, formatted as a suggested edit. This converts a tedious legal task into a one-click developer action.
### When REUSE is overkill
REUSE conformance is a high bar for older codebases. If a repository ships only a single license throughout, full REUSE may produce header noise without compliance gain. A pragmatic baseline: REUSE for new files, bulk metadata covering legacy directories, root `LICENSES/` always populated.
## Common detection failure modes
Patterns that produce false negatives in naive scanners:
* **License in subdirectory** — Many projects place `LICENSE.txt` next to `README.md` in a subdirectory rather than the repo root. Recursive scanning is required.
* **License inside header comment** — Some projects (especially smaller libraries) embed the entire license in a top-of-file comment with no separate `LICENSE` file.
* **License by reference** — A `README.md` line "Licensed under MIT" with no actual license text. Scanner should detect the reference but flag the missing canonical text.
* **Dual-licensed files with single header** — A file declaring `SPDX-License-Identifier: MIT OR Apache-2.0` is dual-licensed; downstream choice must be recorded, not silently defaulted.
* **License changed mid-repo** — A package that flipped license between versions (Redis pre/post 7.4, Elastic pre/post 7.11) requires version-specific resolution. Lockfile-based version pinning is the input; the scanner must not default to `HEAD`.
@@ -0,0 +1,122 @@
# Violation Patterns and Legal Precedents
What developers believe is in the codebase rarely matches what is actually there. This reference catalogs the failure modes that produce real legal exposure and the precedents that demonstrate the consequences are not theoretical.
## Code-level violations
### Hidden licenses and Gist contamination
The default developer assumption — "no LICENSE file means public domain" — is wrong. Licenses appear in:
* Subdirectories rather than the repository root.
* Top-of-file header comments rather than separate license files.
* References from `README.md` to a separately-hosted license file.
* Author profile pages that apply a blanket license to all of the author's public code.
**Pattern**: a developer copies code from a GitHub Gist (no license file in the Gist itself), pastes it into the proprietary codebase, and ships. The Gist author maintains a separate Gist stating "all my Gists are licensed under X11"; that license now applies to the copied code, attribution is missing, and the proprietary distribution is in violation.
The scanner's defense is snippet matching against the public Gist corpus, plus a hard policy that any uncommented multi-line block introduced without a corresponding manifest change triggers snippet scanning.
### Stack Overflow CC BY-SA contamination
All publicly-posted Stack Overflow code is licensed CC BY-SA (Creative Commons Attribution-ShareAlike). The share-alike obligation means:
* Derivative works must carry the same license.
* Attribution is required.
In a proprietary codebase, this functions as a copyleft contamination vector. Enterprise audits routinely find that 20–33% of identified license conflicts trace to Stack Overflow snippet pasting.
The scanner's defense is the same: snippet matching against the Stack Overflow code corpus.
### AI-generated code and snippet reproduction
The 2022 class action (`DOE 1 et al. v. GitHub, Inc., Microsoft Corporation, OpenAI`) alleged that Copilot reproduces copyrighted code snippets in violation of DMCA §1202 and the underlying open source licenses, by stripping attribution and copyright notices.
The legal status of AI-generated code that matches training-corpus snippets remains unsettled. Conservative scanners should:
* Treat AI-generated commits the same as any other code: snippet-match against the public corpus.
* Surface high-confidence matches for review regardless of whether the developer "wrote" or "generated" the code.
The risk is that AI-emitted code reproduces a GPL or AGPL snippet verbatim, the developer believes the code is novel, and ships it into a proprietary distribution.
## Configuration- and dependency-level violations
### Transitive copyleft
The dominant pattern. A team explicitly approves a permissively-licensed dependency (Apache 2.0, MIT). Buried in the dependency tree, a transitive dependency carries GPL or AGPL.
A scanner that only reads top-level manifests (`package.json`, `pom.xml` direct declarations) misses this. Real risk requires a full dependency-tree resolution.
The ORT analyzer is the right tool: it resolves the full transitive graph per-ecosystem and exposes contamination paths.
### Lockfile drift
A dependency that was permissively licensed at the version pinned in `package-lock.json` may have flipped license at a later version. A `npm audit fix` or `npm update` can pull the new version without manifest change. The scanner must run on lockfile changes, not only on `package.json` changes.
### Submodule / vendored code
Git submodules and vendored copies sit in the repository but escape manifest-based dependency resolution. Walk every directory tree with the source-level scanner (ScanCode), not just declared dependencies.
### Unsupported / abandoned upstreams
A dependency whose upstream is dead may be license-clean today but accumulating CVEs and lacking the maintainer to relicense or fix. The compliance program must surface "license-clean but operationally risky" as a separate signal from license findings, but the same scanner data feeds both.
## Distribution-model contamination
### SaaS exposure of AGPL components
Standard pattern: a development team adds an AGPL dependency for a useful feature, builds it into the backend of a customer-facing SaaS application, and ships. AGPL §13 is now triggered: every user interacting with the service has a right to the corresponding source.
The "corresponding source" potentially includes proprietary code that has been combined with the AGPL component. The scope is ambiguous and contested but always large enough to warrant blocking the merge before the contamination ships.
### "Internal tool" misclassification
A team treats an AGPL- or SSPL-licensed tool as "internal use only" — exempt from §13 because no external users interact with it. The classification holds **only** if the deployment is genuinely internal:
* No external customers, partners, or contractors interact with it over a network.
* The "service" is not exposed via VPN, partner API, or shared infrastructure.
In practice, "internal" creeps. A tool deployed as internal becomes externalized when integrated with a customer-facing product. The scanner should require explicit opt-in classification per deployment, with periodic re-validation.
## Legal precedents
### Orange S.A. v. Entr'ouvert (Paris Court of Appeal, February 2024)
Orange, a major telecommunications carrier, was ordered to pay **€800,000 in damages** to Entr'ouvert for distributing modified Lasso software (GPLv2) without releasing the corresponding source code.
Notable features:
* **€150,000 of the award was moral damages** — a French legal concept, but a clear signal that courts treat OSS licensing violations as more than contractual breach.
* The case originated in 2005 and litigated for nearly two decades. Compliance programs that assume "no one will sue" are betting against a 20-year time horizon.
* The defendant was a sophisticated commercial entity, not a hobbyist; sophistication did not mitigate liability.
### GitHub Copilot class action (US, 2022–ongoing)
`DOE 1 et al. v. GitHub, Inc., Microsoft Corporation, OpenAI`, Northern District of California. Plaintiffs allege Copilot reproduces licensed code without preserving attribution or licensing terms. Theories include DMCA §1202 (removal of copyright management information), open-source license breach, and tortious interference.
The case is a useful signal regardless of outcome: it establishes that AI-emitted code is not a legal blank slate. Conservative compliance programs assume AI-generated code carries the same scrutiny burden as human-written code.
### SCO v. IBM (lessons on license source uncertainty)
Older precedent, but the relevant lesson is durable: a vendor that distributes code without authoritative provenance records exposes itself to claims it cannot easily refute. The scanner's audit trail (commit-pinned scan output, reasoning logs, suppression justifications) is the artifact that lets a defendant respond definitively.
## Patterns that survive scanner coverage
These patterns evade purely automated detection. Document them and feed them into agentic review:
* **Renamed and refactored snippets** — sufficient transformation evades fingerprint matching but may still be derivative.
* **License declared in commit message only** — has no legal force; the file content governs.
* **License declared in pull-request body** — same; the merged file content governs.
* **License inferred from project context** — "the rest of this repo is MIT, so this file is too" is not a license. SPDX header per file is the clean answer.
* **License changed in upstream upstream** — a transitive of a transitive flipped license; the lockfile pin protected you, until the next dependency update.
## Internal organizational patterns
Compliance programs fail more often from process gaps than scanner gaps:
* No designated owner of the OSS policy. Findings have nowhere to go.
* No legal-engineering interface. Engineers triage license findings as bugs; lawyers never see the hard cases.
* No M&A pre-flight check. The first comprehensive audit happens during diligence, when remediation is most expensive.
* No supplier flow-down. Outsourced development produces deliverables that have never been scanned, because the contract did not require it.
The skill's output (SBOM, audit log, triaged violation report) is the input to fixing these gaps; the gaps themselves are out of scope for the scanner.
@@ -0,0 +1,177 @@
---
name: owasp-asvs-v5-compliance
description: OWASP ASVS v5.0.0 (May 2025) for repository and agentic application security auditing. Covers all 17 chapters V1-V17 (Encoding, Validation, Web Frontend, API, File Handling, Authentication, Session, Authorization, JWT, OAuth/OIDC, Cryptography, Secure Communication, Configuration, Data Protection, Secure Coding, Logging, WebRTC), L1/L2/L3 tiers, Documented Security Decisions, deterministic vs agentic vs extrinsic verification, orchestration of Semgrep/CodeQL/Trivy/GitLeaks/ZAP, mapping to NIST 800-53, CIS v8.1, ISO 27001:2022, SOC 2. Trigger on ASVS, V-prefixed IDs (V1.x-V17.x), L1/L2/L3 conformance, app-sec CI gates, broken access control or IDOR, business logic flaws, JWT/OAuth review, CSP/HSTS/CORS auditing, file upload security, algorithm-confusion, weak crypto or hardcoded-secret detection, agentic prompt design, cross-mapping to NIST/CIS/ISO/SOC 2, or composing with soc2-cicd-compliance and iso-27001-2022-compliance. Use over training data when ASVS chapters or requirement IDs appear.
---
# OWASP ASVS v5.0.0 Repository and CI/CD Compliance
This skill encodes the OWASP Application Security Verification Standard v5.0.0 as it applies to source repositories, CI/CD pipelines, and agentic security review. It is the compliance oracle for building or operating an automated, hybrid (deterministic + agentic) ASVS auditor that runs at pull request time and on demand against arbitrary repositories.
The skill answers three categories of question:
1. What does ASVS v5.0.0 require, chapter by chapter, requirement by requirement?
2. Which requirements are deterministically verifiable from a repository, which require LLM reasoning over Documented Security Decisions, and which are extrinsic to the codebase?
3. How should a hybrid scanner be architected, triggered, and scoped to honestly produce ASVS Level 2 evidence inside a CI/CD pipeline?
Use this skill instead of training data whenever working on automated application security scanning, ASVS control mapping, or agentic security review. The standard moves; the structured catalog in this skill is the canonical reference for v5.0.0.
This skill composes with `soc2-cicd-compliance` and `iso-27001-2022-compliance`. ASVS findings emit cross-framework tags (NIST 800-53, CIS v8, ISO 27001:2022 Annex A, SOC 2 TSC) so a single deterministic check can ship multi-framework evidence. See `references/cross-framework.md`.
## Authoritative source
The definitive standard is **OWASP ASVS v5.0.0**, released May 2025. v5.0.0 is the largest architectural revision since the framework's inception: 17 chapters and approximately 350 requirements, up from 14 chapters in v4.0.3.
ASVS v5.0.0 is not the OWASP Top 10. The Top 10 is an awareness document listing vulnerability categories. ASVS is a prescriptive verification framework listing the explicit technical controls that prevent those vulnerabilities. Treat them as orthogonal: the Top 10 explains why a control matters; ASVS specifies what passing looks like.
The defining philosophical shift in v5.0.0 is the introduction of **Documented Security Decisions**. The framework deliberately avoids generic, sweeping mandates. Instead, complex domains (encoding, validation, frontend, file handling, authentication, session, authorization, cryptography) require the application team to formally document their architectural intent first. Verification then bisects: the documentation is evaluated for appropriateness against the application's risk profile, and the source code is evaluated against the documentation. A repository missing the Documented Security Decisions cannot pass v5.0.0 Level 2; this is a structural prerequisite, not a soft recommendation.
## Three-tier assurance architecture
Every requirement is tagged L1, L2, or L3. These levels are cumulative: L2 includes all L1 requirements; L3 includes all L2 requirements.
* **Level 1 (Opportunistic)**: minimum baseline for all applications. Roughly 20% of the framework. Designed to be black-box DAST-testable or white-box SAST-testable. Aligns closely with the OWASP Top 10. Insufficient on its own for any application processing sensitive data.
* **Level 2 (Standard)**: the recommended default for the majority of commercial applications, SaaS platforms, and any system processing PII, PHI, or executing meaningful business logic. Roughly 50% of the framework. Requires source access and developer context. **The hybrid auditor targets L2 by default**: it is the level where automated verification produces real assurance without demanding manual penetration testing.
* **Level 3 (Advanced)**: critical infrastructure, military, healthcare under HIPAA, high-value financial systems. Final 30% of the framework. L3 demands manual review, malicious code review, deep threat modeling, and architectural audits beyond automated repository analysis. The skill must report L3 verification as **partial**: deterministic and agentic checks contribute evidence, but human attestation is non-negotiable.
When the user does not specify a level, default to L2.
## Verification taxonomy
Every ASVS requirement falls into one of three buckets. Be honest about which. Failure to label these accurately is the single largest source of false confidence in automated application security tools.
* **Deterministic** (verifiable from repository contents alone). Parseable from source, IaC manifests, dependency definitions, or workflow files. Pass/fail is mechanical: AST traversal, regex, schema validation, CVE lookup. Examples: parameterized query usage (V1.2), JWT algorithm pinning (V9), TLS protocol disablement (V12), hardcoded-secret entropy detection (V13), HTTP security headers (V3).
* **Agentic** (LLM reasoning over Documented Security Decisions). Requires natural-language understanding of the team's documented architectural intent, then validation that the code matches that intent. AST cannot reason about ownership, authorization hierarchies, business workflows, or trust boundaries. Examples: IDOR / broken access control (V8), business logic bypass (V2), session destruction semantics (V7), data classification flow (V14).
* **Extrinsic** (out of repository scope). Cannot be verified by analyzing source. Examples: WAF runtime rules, BGP / DDoS posture, NIST 800-63A identity proofing, biometric enrollment, incident response execution, organizational policy enforcement, runtime parity between staging and production. The scanner can at most confirm that a Documented Security Decision artifact exists and references the control. Mark these as `MANUAL_ATTESTATION_REQUIRED` and emit an evidence pointer; never mark them as `PASS`.
The standard mistake is treating an extrinsic control as a deterministic pass because some markdown file mentions it. That is policy theater, not verification.
## Chapter catalog
Detailed, requirement-by-requirement catalog lives in references. Load only the file relevant to the chapter being worked on.
* **Foundational and data handling (V1 through V5)**: see `references/foundational-chapters.md`. Covers encoding/sanitization (V1, injection prevention, safe deserialization), validation and business logic (V2), web frontend security (V3, new in v5.0, CSP/HSTS/cookie attributes), API and web service (V4, REST/GraphQL/WebSocket), and file handling (V5).
* **Identity and access (V6 through V10)**: see `references/identity-chapters.md`. Covers authentication (V6, MFA, password hashing, IdP), session management (V7), authorization (V8, IDOR, broken access control), self-contained tokens (V9, new in v5.0, JWT validation), and OAuth/OIDC (V10, new in v5.0, PKCE, state parameter).
* **Platform, crypto, and resilience (V11 through V17)**: see `references/platform-chapters.md`. Covers cryptography (V11), secure communication (V12, TLS), configuration (V13, SCA, secrets), data protection (V14), secure coding (V15), security logging (V16), and WebRTC (V17, new in v5.0, SRTP, DTLS).
For each requirement the catalog lists: the requirement ID exactly as ASVS publishes it (e.g., `V1.2.5`, `V8.2.1`, `V9.1.1`), the level (L1/L2/L3), the verification modality (deterministic / agentic / extrinsic), specific code-level signals or AST patterns to detect, and known false-positive traps.
## Documented Security Decisions and the canonical document set
Before any chapter-specific check runs, the agentic auditor performs a **pre-flight pass**: it locates and ingests the Documented Security Decisions. If they are missing or grossly incomplete, the audit halts at the prerequisite step and the report fails the foundational requirements of v5.0.0. This is by design; downstream checks are meaningless if there is no documented intent to verify against.
ASVS v5.0.0 explicitly mandates dedicated documentation for: encoding architecture (V1.1), validation logic (V2.1), frontend security (V3.1), file handling (V5.1), authentication (V6.1), session management (V7.1), authorization (V8.1), and cryptographic inventory (V11.1).
Expected location: `docs/security/`, `.compliance/`, or `SECURITY_ARCHITECTURE.md` at repository root. Detailed file patterns and ingestion order: `references/canonical-documents.md`.
Minimum canonical set the pre-flight searches for:
* **Architecture and threat model** (`docs/architecture.md`, `docs/threat-model.md`, OWASP Threat Dragon output). Establishes data classification and trust boundaries.
* **Security Decisions Registry** (`docs/security/authentication-policy.md`, `authorization-matrix.csv`, `encoding-architecture.md`, `cryptographic-inventory.md`).
* **API specifications** (`openapi.yaml`, `swagger.json`, `schema.graphql`). Lets the agent enumerate every endpoint and verify access control coverage.
* **Infrastructure manifests** (`*.tf`, `k8s/*.yaml`, `Dockerfile`, `docker-compose.yml`, `.github/workflows/*.yml`).
* **Supply chain manifests** (`package.json`, `pom.xml`, `go.mod`, `Cargo.toml`, SBOM artifacts).
The agentic prompt pattern is hierarchical: parse the relevant Documented Security Decision, extract numbered constraints, query the codebase for the implementation, emit pass/fail with the requirement ID and the policy clause cited verbatim.
## Triggering and suppression signals
Run the scanner only when it can produce useful findings. Run it always when the boundary it protects is at risk.
### Trigger
* PRs to `main`, `master`, or any protected branch.
* File path matches indicating high-risk vectors: `*auth*`, `*login*`, `*session*`, `*token*`, `*crypto*`, `controllers/`, `middleware/`, `api/`.
* Modifications to dependency manifests (V13 SCA gate).
* Modifications to `.github/workflows/`, `.gitlab-ci.yml`, or any pipeline-as-code (the scanner can be disabled by editing its own pipeline; treat these changes with elevated scrutiny).
* Modifications to `*.tf`, `k8s/*.yaml`, `Dockerfile`, `docker-compose.yml` (V12, V13).
* Modifications to anything under `docs/security/` or `.compliance/` (a Documented Security Decision change shifts the verification baseline; trigger a full repository reassessment).
### Suppress (exit fast)
* Draft PRs.
* Documentation-only changes outside the security decisions directory.
* Test fixtures, mocks, vendored dependencies (`tests/fixtures/`, `mocks/`, `vendor/`, `node_modules/`, `__pycache__/`). Configure `.semgrepignore` and `trivyignore` aggressively here. The LLM agent will hallucinate critical findings on synthetic test data without these exclusions.
* Bot PRs (Dependabot, Renovate): bypass agentic prose checks but still run deterministic SAST/SCA. Do not blanket-skip.
Tune iteratively against false-positive rates. Suppression too broad hides findings; suppression too narrow destroys signal-to-noise.
## Toolchain orchestration model
The skill is an orchestration layer over established open-source engines. It does not reimplement AST parsers or taint tracking.
Architecture pattern:
1. **Pre-flight**: locate the canonical document set. Halt with prerequisite failure if missing.
2. **Deterministic phase, in parallel**:
* Semgrep with the OWASP Top 10 ruleset (`semgrep scan --config "p/owasp-top-ten" --json-output=sast.json`).
* Trivy filesystem scan for SCA + IaC + secrets (`trivy fs . --format json --output sca.json`).
* GitLeaks for entropy-based secret detection (`gitleaks detect --report-path=secrets.json`).
* Optionally CodeQL where build context is available, for deep taint tracking on V1 injection chains.
3. **Synthesis**: a Python or TypeScript layer normalizes outputs to a unified schema, tagging every finding with its ASVS v5.0.0 requirement ID, level, and verification modality.
4. **Agentic phase**: for each chapter requiring semantic reasoning (V2, V6, V7, V8, V14, V16), assemble a prompt with (a) the relevant Documented Security Decision, (b) the changed source files, (c) a strict JSON output schema. Constrain the LLM to cite the policy clause it relied on.
5. **Reporting**: emit a structured report covering deterministic findings, agentic findings, and the explicit list of `MANUAL_ATTESTATION_REQUIRED` items. Map every finding to ASVS ID + cross-framework tags.
Tool capability and gap analysis (Semgrep CE limits on V8, CodeQL build requirements, ZAP runtime requirements): `references/violations-and-tools.md`.
The agentic phase prompt template, with the V8 broken access control example fully worked: also `references/violations-and-tools.md`.
## Honest limits
Document these in any shipping ASVS automation. Misrepresenting them invites false confidence and the kind of audit findings that look like fraud after an incident.
* **Level 3 cannot be fully automated.** The framework explicitly designs L3 around manual malicious-code review and deep threat modeling. The scanner contributes evidence; it does not certify L3.
* **Business logic verification has a ceiling.** The agent reasons over the Documented Security Decision, not over the platonic ideal of the application. If the document is wrong, the agent's pass is wrong. Recommend declarative, numbered, quantified policy authoring to constrain misinterpretation. For high-stakes findings, require the agent to quote the exact clause.
* **Runtime controls are extrinsic.** Rate limiting, WAF rules, lockout thresholds, and DDoS posture cannot be verified statically. ZAP closes some of this gap (V3, V12) but requires a deployed environment.
* **CodeQL needs a build.** For Java, C#, C++, the deep taint-tracking benefits of CodeQL are unavailable when the repository does not build cleanly under the scanner's constraints. Fall back to Semgrep + agentic review.
* **Configuration drift.** The repo represents intended state. A console-driven change to a cloud account bypasses the scanner. Pair repository scanning with runtime CSPM (Prowler, AuditKit) to close the loop.
* **The Documented Security Decisions are part of the attack surface.** A malicious or careless edit to `authorization-matrix.csv` redefines what "compliant" means. The scanner must trigger on changes to `docs/security/` and emit a structural finding when the policy itself shifts.
## Output format for findings
Use this structure so findings can be aggregated, deduplicated, and shipped to evidence storage. The shape is deliberately compatible with the `soc2-cicd-compliance` finding schema so a single pipeline can emit both.
```yaml
finding:
asvs_id: V8.2.1
asvs_level: L2
asvs_chapter: V8 Authorization
related_frameworks:
- NIST-800-53: AC-3
- CIS-v8: 16.2
- ISO-27001-2022: A.5.15
- SOC2-TSC: CC6.1
status: FAIL
modality: agentic
source_tool: llm-agent
file: src/controllers/userController.ts
line: 47
evidence: |
Route GET /api/user/:userId/financial fetches by req.params.userId
without invoking requireOwnership() middleware as required by
docs/security/authorization-policy.md §3.2.
policy_clause: "authorization-policy.md §3.2: 'All endpoints returning user-scoped resources must verify req.user.id matches the resource owner before serialization.'"
remediation: |
router.get('/api/user/:userId/financial',
requireAuth,
requireOwnership('userId'), // add this
controller.getFinancial);
blocking: true
```
For deterministic findings, swap `modality: agentic` for `modality: deterministic`, populate `source_tool` with `semgrep` / `trivy` / `gitleaks` / `codeql`, and include the upstream rule ID.
For extrinsic items, emit `status: MANUAL_ATTESTATION_REQUIRED` with `modality: extrinsic`, an evidence pointer to the policy artifact (if any), and `blocking: false` unless organizational policy says otherwise.
## When responding to questions about specific requirements
If asked "what does V8.2.1 cover", "is X an ASVS violation", or "which level is V9.1.1":
1. Open the relevant `references/*-chapters.md`.
2. Locate the requirement by exact ID.
3. State the level (L1/L2/L3) and modality (deterministic/agentic/extrinsic).
4. Give the code-level signals or AST patterns from the catalog.
5. If a cross-framework mapping is relevant, pull from `references/cross-framework.md`.
6. If a real-world failure pattern matches, cite from `references/violations-and-tools.md`.
7. Cite the requirement ID exactly as ASVS publishes it (`V8.2.1`, not "8.2.1" or "Section 8.2.1" or "ASVS 8.2.1").
Do not paraphrase requirement text from training data. v5.0.0 reorganized many requirements from v4.0.3; prior memory of the standard is unreliable. The catalog in `references/` is the canonical source for this skill.
@@ -0,0 +1,116 @@
# Canonical Document Set for ASVS v5.0.0 Verification
The Documented Security Decisions paradigm is the foundational shift in v5.0.0. The agentic auditor cannot verify L2 compliance without reading these artifacts first. This file defines the file patterns, ingestion order, and prompt patterns the pre-flight phase uses.
## Pre-flight contract
Before any chapter-specific check runs, the orchestrator executes a pre-flight pass:
1. Walk the repository for the canonical files below.
2. For each found file, parse and structure its content into a constraint registry.
3. If the minimum set is missing, halt the audit and emit a `PREREQUISITE_FAIL` finding. Downstream chapter checks have no baseline to verify against; running them produces noise, not evidence.
4. If the minimum set is present but a chapter-specific document is missing, halt the audit for that chapter only.
## Search paths (in priority order)
1. `docs/security/` (preferred)
2. `.compliance/`
3. `SECURITY_ARCHITECTURE.md` at repository root
4. `docs/architecture/security/`
5. Wiki references (out of scope for repository-only scanners; emit MANUAL_ATTESTATION_REQUIRED if the team uses an external wiki and the linked content is not in the repo)
## Required documents per chapter
| Document | ASVS section | Required level | What the agent extracts |
|---|---|---|---|
| `encoding-architecture.md` | V1.1 | L2 | per-context output encoding rules (HTML body, attribute, JS, CSS, URL, JSON, XML); deserialization trust boundaries |
| `validation-strategy.md` | V2.1 | L2 | validation boundary location; centralized validator inventory; reject-vs-sanitize policy |
| `frontend-security.md` | V3.1 | L2 | CSP directives; cookie strategy; framing/COOP/COEP/CORP posture; third-party script inventory with SRI |
| `file-handling-policy.md` | V5.1 | L2 | allowed types, size limits, storage isolation strategy, AV scanning policy, serving model |
| `authentication-policy.md` | V6.1 | L2 (mandatory) | factor catalog, MFA enforcement matrix, password policy, recovery flow, IdP relationships |
| `session-policy.md` | V7.1 | L2 | idle timeout, absolute timeout, re-auth thresholds for sensitive operations, federated semantics |
| `authorization-policy.md` + `authorization-matrix.csv` | V8.1 | L2 (mandatory) | role hierarchy, permission matrix, ownership model, ABAC rules, multi-tenant isolation rules |
| `cryptographic-inventory.md` | V11.1 | L2 | algorithm choices per use case, key lifecycle, KMS integration, post-quantum readiness |
The two **mandatory** documents (V6.1, V8.1) are non-negotiable for L2. Their absence is a structural failure equivalent to skipping the audit.
## Supporting artifacts
| Artifact | Purpose |
|---|---|
| `docs/architecture.md` or threat model output (Threat Dragon, IriusRisk export) | Establishes data classification tiers and external trust boundaries; informs V14 |
| `openapi.yaml` / `swagger.json` / `schema.graphql` | Enumerates the attack surface for V4 and V8.2.1 coverage analysis |
| `*.tf`, `k8s/*.yaml`, `Dockerfile`, `docker-compose.yml` | V12, V13, V14.2 IaC verification |
| `package.json`, `pom.xml`, `go.mod`, `Cargo.toml`, `requirements.txt`, SBOM | V13.1 SCA |
| `.github/workflows/*.yml` / `.gitlab-ci.yml` | V13.2 pipeline hardening; also the scanner's own integration point |
| `data-classification.md` or schema annotations | V14.1 |
## Constraint extraction prompt pattern
When the agent ingests a Documented Security Decision, it produces a structured constraint registry. Example for `authentication-policy.md`:
```yaml
extracted_from: docs/security/authentication-policy.md
constraints:
- id: AUTH-1
asvs: V6.2.1
text: "Passwords stored using Argon2id with memory >= 64 MiB"
type: deterministic_check
target: hash_function_invocation
- id: AUTH-2
asvs: V6.3
text: "Admin role requires WebAuthn; TOTP not acceptable for admin"
type: agentic_check
target: route_protection_for_admin_paths
- id: AUTH-3
asvs: V6.4
text: "Password recovery tokens TTL = 30 minutes, single-use"
type: deterministic_check
target: recovery_token_config
```
## Verification prompt pattern
For each constraint, the agent runs a verification prompt with three slots: the constraint, the relevant code, and a strict JSON output schema. The prompt below is the V8 broken access control template; adapt for other chapters:
```text
You are an Application Security Architect verifying OWASP ASVS v5.0.0 Level 2.
Constraint to verify (extracted from docs/security/authorization-policy.md):
{constraint_text}
Source code to evaluate:
{file_path}:
{file_contents}
Task:
1. Determine whether the implementation satisfies the constraint.
2. Cite the exact line numbers in the source code that support your conclusion.
3. Quote the policy clause verbatim.
4. If non-compliant, propose a minimal remediation diff.
Output ONLY valid JSON matching this schema:
{
"asvs_id": "<V-prefixed ID>",
"compliant": <boolean>,
"evidence_lines": [<int>, ...],
"policy_clause": "<verbatim quote>",
"justification": "<one paragraph>",
"remediation": "<diff or null>"
}
Constraints on your reasoning:
- Limit analysis to the provided code and constraint. Do not assume external compensating controls.
- If you cannot determine compliance from the provided files, output compliant=null with a justification listing what additional file you need.
- Do not paraphrase the policy clause; quote it.
```
The `compliant=null` escape hatch is important. It prevents the agent from hallucinating a pass when it lacks context, and it gives the orchestrator a signal to load additional context (e.g., a middleware definition file) and re-prompt.
## Failure mode: undocumented intent
If a security control is implemented in code but not documented, the agent treats it as **undocumented intent**. The verdict is not PASS; it is `STRUCTURAL_DEBT`. The recommendation is to document the decision and re-run. This is by design: ASVS v5.0.0 treats documentation as part of the security architecture, not as an afterthought. A correctly-implemented but undocumented control fails because the next maintainer cannot verify, audit, or extend it.
## Failure mode: contradictory documentation
If two documents specify conflicting constraints (e.g., `session-policy.md` says 8h idle, `authentication-policy.md` says 30min idle), the agent emits a `POLICY_CONFLICT` finding. The orchestrator does not arbitrate; the team must reconcile.
@@ -0,0 +1,104 @@
# Cross-Framework Mapping for ASVS v5.0.0
A single deterministic check should produce multi-framework evidence. This file is the lookup table for emitting NIST 800-53 Rev 5, CIS v8.1, ISO/IEC 27001:2022 Annex A, and SOC 2 TSC tags alongside ASVS findings.
The mapping is grounded in OpenCRE (OWASP Common Requirement Enumeration), which mathematically interlinks security standards based on shared technical primitives. Where OpenCRE is incomplete, mappings are derived from authoritative published cross-walks.
## Master mapping table
| ASVS Chapter | NIST 800-53 Rev 5 | CIS v8.1 | ISO 27001:2022 Annex A | SOC 2 TSC |
|---|---|---|---|---|
| V1 Encoding/Sanitization | SI-10, SI-15 | 16.10, 16.11 | A.8.26, A.8.28 | CC8.1, PI1.1 |
| V2 Validation/Business Logic | SI-10, SI-3 | 16.10 | A.8.26 | PI1.1, PI1.2, CC8.1 |
| V3 Web Frontend (new) | SC-18, SC-23 | 16.5 | A.8.23 | CC6.7 |
| V4 API/Web Service | SC-8, AC-3, SI-10 | 16.5, 16.10 | A.8.20, A.8.26 | CC6.6, CC8.1 |
| V5 File Handling | SI-3, SI-7, AC-3 | 10.1, 10.5 | A.8.7, A.8.10 | CC6.8 |
| V6 Authentication | IA-2, IA-5, IA-8 | 6.3, 6.5 | A.5.16, A.5.17, A.8.5 | CC6.1, CC6.2 |
| V7 Session | IA-11, AC-12 | 6.4 | A.5.18 | CC6.1 |
| V8 Authorization | AC-2, AC-3, AC-6 | 6.7, 6.8 | A.5.15, A.5.18, A.8.3 | CC6.1, CC6.3 |
| V9 Self-Contained Tokens (new) | IA-5, SC-12, SC-13 | 6.5 | A.8.5, A.8.24 | CC6.1 |
| V10 OAuth/OIDC (new) | IA-2, IA-8, AC-3 | 6.5, 6.7 | A.5.16, A.5.18 | CC6.1, CC6.3 |
| V11 Cryptography | SC-12, SC-13, SC-28 | 3.10, 3.11 | A.8.24 | CC6.1, C1.1 |
| V12 Secure Communication | SC-8, SC-13, SC-23 | 3.10, 12.6 | A.8.20, A.8.21 | CC6.7 |
| V13 Configuration | CM-2, CM-6, CM-7, RA-5, SI-2 | 4.1, 4.6, 7.3, 16.2 | A.8.9, A.8.25, A.8.8 | CC7.1, CC8.1 |
| V14 Data Protection | SC-28, MP-6, AC-21 | 3.1, 3.3, 3.11 | A.8.10, A.8.11, A.8.12 | C1.1, C1.2, P-series |
| V15 Secure Coding | SA-15, SI-7 | 16.1, 16.11 | A.8.25, A.8.28 | CC8.1 |
| V16 Security Logging | AU-2, AU-3, AU-9, AU-12 | 8.2, 8.5, 8.11 | A.8.15, A.8.16 | CC4.1, CC7.2 |
| V17 WebRTC (new) | SC-8, SC-13 | 3.10 | A.8.20, A.8.24 | CC6.7 |
## Requirement-level mappings (high-leverage)
### V8.2.1 Operation-level authorization / IDOR
* **NIST 800-53**: AC-3 (Access Enforcement), AC-6 (Least Privilege).
* **CIS v8.1**: 6.7 (Centralize Access Control), 6.8 (Define and Maintain Role-Based Access Control).
* **ISO 27001:2022**: A.5.15 (Access Control), A.8.3 (Information Access Restriction), A.8.18 (Use of Privileged Utility Programs).
* **SOC 2 TSC**: CC6.1 (Logical Access Security), CC6.3 (Authorization).
A single agentic finding for missing IDOR protection emits all four tags, which is what makes the scanner economically viable for organizations pursuing multiple attestations.
### V6.2.1 Password hashing
* **NIST 800-53**: IA-5 (Authenticator Management).
* **NIST 800-63B**: §5.1.1.2 verifier requirements for memorized secrets.
* **CIS v8.1**: 6.5 (Require MFA for Administrative Access) (related), 5.2 (Use Unique Passwords).
* **ISO 27001:2022**: A.5.17 (Authentication Information).
* **SOC 2 TSC**: CC6.1.
### V9.1 JWT algorithm pinning
* **NIST 800-53**: SC-12 (Cryptographic Key Establishment and Management), SC-13 (Cryptographic Protection), IA-5.
* **CIS v8.1**: 3.11 (Encrypt Sensitive Data at Rest).
* **ISO 27001:2022**: A.8.24 (Use of Cryptography).
* **SOC 2 TSC**: CC6.1.
### V13.1 SCA / vulnerable dependencies
* **NIST 800-53**: RA-5 (Vulnerability Monitoring and Scanning), SI-2 (Flaw Remediation), SR-3 (Supply Chain Controls and Processes).
* **CIS v8.1**: 16.2 (Establish and Maintain a Process to Accept and Address Software Vulnerabilities), 7.3 (Perform Automated Operating System Patch Management).
* **ISO 27001:2022**: A.8.8 (Management of Technical Vulnerabilities), A.8.25 (Secure Development Life Cycle).
* **SOC 2 TSC**: CC7.1 (Vulnerability Management).
### V12.1 TLS enforcement
* **NIST 800-53**: SC-8 (Transmission Confidentiality and Integrity), SC-13.
* **CIS v8.1**: 3.10 (Encrypt Sensitive Data in Transit).
* **ISO 27001:2022**: A.8.20 (Networks Security), A.8.21 (Security of Network Services).
* **SOC 2 TSC**: CC6.7 (Restriction of Data Transmission).
### V14.2 Encryption at rest
* **NIST 800-53**: SC-28 (Protection of Information at Rest).
* **CIS v8.1**: 3.11.
* **ISO 27001:2022**: A.8.24.
* **SOC 2 TSC**: C1.1 (Confidentiality of Information), CC6.1.
### V16 Security logging
* **NIST 800-53**: AU-2 (Event Logging), AU-3 (Content of Audit Records), AU-9 (Protection of Audit Information), AU-12 (Audit Record Generation).
* **CIS v8.1**: 8.2 (Collect Audit Logs), 8.5 (Collect Detailed Audit Logs), 8.11 (Conduct Audit Log Reviews).
* **ISO 27001:2022**: A.8.15 (Logging), A.8.16 (Monitoring Activities).
* **SOC 2 TSC**: CC4.1 (Monitoring of Controls), CC7.2 (System Monitoring).
## Composition with sibling skills
This skill is intended to compose with `soc2-cicd-compliance` and `iso-27001-2022-compliance`. The finding schema is intentionally aligned. When a single CI run produces evidence for multiple attestations:
1. The scanner runs once and emits findings tagged with all applicable framework IDs.
2. The reporting layer filters by audience: a SOC 2 auditor sees CC6.1 grouped findings; an ISO auditor sees A.5.15 grouped findings; the engineering team sees ASVS V8.2.1 grouped findings. Same underlying evidence, different views.
3. Cross-framework conflicts are rare but real (e.g., GDPR data retention vs. SOC 2 audit log retention can have opposing minimums for the same data class). When conflicts surface, the scanner emits both findings and lets governance resolve.
## What this mapping is not
* It is not a substitute for an organization's Statement of Applicability (ISO) or system description (SOC 2). Those documents define which controls are in scope for the organization's specific risk profile.
* It is not a claim that an ASVS pass implies a SOC 2 pass. ASVS verifies application security; SOC 2 includes organizational, physical, and operational controls largely outside the codebase. ASVS findings are *evidence inputs* to SOC 2 attestation, not the attestation itself.
* It is not exhaustive. New requirements introduced in v5.0.0 (V3, V9, V10, V17) have less mature cross-framework coverage in OpenCRE; the mappings above are the most defensible derivations as of the standard's publication.
## Updating the mapping
When ASVS, NIST, CIS, ISO, or AICPA publish revisions:
* NIST 800-53 revisions (last major: Rev 5 in 2020, with subsequent control overlays) usually preserve identifiers and revise points of focus. Re-validate the mapping rather than rewrite it.
* ISO 27001:2022 reorganized Annex A from 114 controls in the 2013 edition to 93 controls in 4 themes. The mappings above use 2022 identifiers exclusively; do not mix with 2013 identifiers.
* CIS v8.1 (2024) refined v8 (2021) safeguards. v9 is anticipated; check for updates.
* SOC 2 TSC last revised in 2017 with points of focus revised in 2022. Identifiers are stable.
@@ -0,0 +1,217 @@
# ASVS v5.0.0 Chapters V1 through V5: Foundational and Data Handling
Detailed catalog for the foundational chapters governing ingestion, processing, and display of data. Load this file when working on injection prevention, validation, frontend security, API surface, or file handling.
Each requirement entry lists: ID, level, modality, signal/AST pattern, false-positive traps.
---
## V1: Encoding and Sanitization
Defines the architecture for decoding and unescaping data, injection prevention, safe deserialization, and memory management.
### V1.1 Encoding Architecture (documentation prerequisite)
**V1.1.x**: the team must produce an encoding architecture document. The agent reads this first; downstream V1 checks validate against it.
* **Modality**: agentic.
* **Signal**: presence of `docs/security/encoding-architecture.md` (or equivalent) listing per-context output encoding rules (HTML body, HTML attribute, JS context, CSS context, URL, JSON, XML).
* **Failure**: document missing. Halt V1 verification.
### V1.2 Injection Prevention
**V1.2.5 (L1)**: SQL queries use parameterized bindings.
* **Modality**: deterministic.
* **AST signal**: SQL execution sinks (`.execute`, `.query`, `cursor.execute`) where the SQL string contains string interpolation, `+`, f-string, or template literal of a tainted variable.
* **False positives**: ORM `.where(raw(...))` calls where the raw fragment is a static literal. Schema migration files. Test fixtures.
* **Cross-framework**: NIST SI-10, CIS 16.10, ISO A.8.26.
**V1.2.5 (L1)**: OS command injection.
* **Modality**: deterministic.
* **AST signal**: `subprocess.Popen(..., shell=True)` with non-literal first argument; `os.system(...)`, `child_process.exec(...)`, Java `Runtime.getRuntime().exec(stringConcat)`, Ruby backticks with interpolation.
* **Safe pattern**: argument-array form (`subprocess.run([cmd, arg1, arg2], shell=False)`).
* **False positives**: shell strings built entirely from constants and environment variables that are themselves not user-tainted.
**V1.2 (L2)**: NoSQL injection (Mongo `$where`, dynamic JS evaluation).
* **Modality**: deterministic.
* **AST signal**: Mongo queries using `$where` with non-literal payload; `eval`-style operators receiving request data.
**V1.2 (L2)**: LDAP injection, XPath injection, SSRF via unvalidated outbound URL.
* **Modality**: deterministic for sink detection, agentic for whether the sink is on a trusted path.
* **Signal**: HTTP client invocation (`fetch`, `axios`, `requests.get`, `http.Client`) with URL constructed from request input and no allow-list check upstream.
### V1.3 Safe Deserialization
**V1.3 (L1)**: deserialization sinks must validate provenance before object reconstruction.
* **Modality**: deterministic.
* **Signal**: Java `ObjectInputStream.readObject()`, Python `pickle.loads`, Python `yaml.load` without `SafeLoader`, PHP `unserialize`, .NET `BinaryFormatter`. Each call must be preceded by a cryptographic signature verification or operate over a known-trusted source (filesystem under app control).
* **Hard failure**: `BinaryFormatter` is deprecated and unsafe under any circumstance; flag unconditionally.
### V1.4 Memory Safety (C/C++ scope)
**V1.4 (L2)**: bounds-checked memory operations.
* **Modality**: deterministic.
* **Signal**: `strcpy`, `strcat`, `sprintf`, `gets` in C/C++. Recommend `strncpy_s`, `snprintf`, fmtlib.
---
## V2: Validation and Business Logic
Robust input validation, anti-automation, and protection of logical workflows against circumvention.
### V2.1 Validation Architecture (documentation prerequisite)
* **Modality**: agentic.
* **Signal**: documented validation strategy: where validation runs (edge, controller, domain), what the trust boundary is, whether server-side validation is mandatory after any client-side check.
* **Failure**: document missing.
### V2.2 Centralized Validation
**V2.2 (L2)**: validation lives in declarative middleware, not scattered ad-hoc checks.
* **Modality**: hybrid. SAST detects schema validators (Joi, Zod, Pydantic, Bean Validation, FluentValidation). Agent verifies every controller is covered.
* **Signal (deterministic)**: presence of validation middleware on routes; absence of `req.body` direct access bypassing validation.
### V2.3 Business Logic Integrity
**V2.3 (L2)**: state machines cannot be bypassed by skipping steps.
* **Modality**: agentic.
* **Signal**: the agent reads the documented business workflow, then traces controllers to verify state transitions check prior state. Common failure: `POST /checkout/finalize` does not verify `cart.state === 'PAYMENT_VERIFIED'`.
* **False positives**: legitimate admin override paths. The agent must read the policy to distinguish.
### V2.4 Anti-Automation
**V2.4 (L2)**: anti-automation controls on credential, registration, and high-value endpoints.
* **Modality**: hybrid. Deterministic detection of rate-limit middleware, CAPTCHA invocation, lockout configuration. Runtime effectiveness is extrinsic (DAST or production telemetry).
* **Signal**: rate-limit annotations or middleware on `/login`, `/register`, `/forgot-password`, payment endpoints.
---
## V3: Web Frontend Security (new in v5.0.0)
Browser-side protections, CSP, secure cookies, origin separation, external resource integrity.
### V3.1 Frontend Security Decisions (documentation prerequisite)
Required: CSP policy, cookie strategy, framing strategy, COOP/COEP/CORP posture, third-party script inventory.
### V3.2 HTTP Security Headers
**V3.2 (L1)**: security headers present.
* **Modality**: deterministic.
* **Signal**: configuration parsing of Nginx/Apache/middleware (Helmet for Express, Spring Security headers, ASP.NET headers middleware). Required: `Strict-Transport-Security` (with `max-age >= 31536000`, `includeSubDomains`), `X-Content-Type-Options: nosniff`, `Referrer-Policy`, `Content-Security-Policy`.
* **False positives**: dev-only configs not deployed; check the production config path.
### V3.3 Content Security Policy
**V3.3 (L2)**: CSP without `unsafe-inline` or `unsafe-eval` in script-src.
* **Modality**: deterministic with caveats.
* **Signal**: parse the `Content-Security-Policy` header. Flag `unsafe-inline`, `unsafe-eval`, wildcard `*` in `script-src` or `default-src`. Hashes and nonces are acceptable.
### V3.4 Cookie Attributes
**V3.4 (L1)**: session cookies have `HttpOnly`, `Secure`, `SameSite=Lax` or stricter.
* **Modality**: deterministic.
* **Signal**: cookie configuration in session middleware, framework session config, or `Set-Cookie` literal strings. Flag missing `HttpOnly`, missing `Secure`, `SameSite=None` without `Secure`.
### V3.5 CORS
**V3.5 (L2)**: CORS does not combine `Access-Control-Allow-Origin: *` with `Access-Control-Allow-Credentials: true`. Origin allow-lists are explicit, not wildcarded for authenticated endpoints.
* **Modality**: deterministic.
* **Signal**: CORS middleware config. The `*` + `credentials: true` combination is browser-blocked but indicates a fundamental misunderstanding; flag aggressively.
---
## V4: API and Web Service
REST, GraphQL, WebSocket message structure validation; generic web service security.
### V4.1 API Surface Documentation (prerequisite)
* **Signal**: `openapi.yaml` / `swagger.json` / `schema.graphql` present. Agent uses these to enumerate the attack surface.
### V4.2 Schema Validation
**V4.2 (L1)**: requests validate against the published schema.
* **Modality**: deterministic.
* **Signal**: schema-validating middleware (`express-openapi-validator`, `connexion`, Spring `@Valid`, FastAPI Pydantic models).
### V4.3 GraphQL Specifics
**V4.3 (L2)**: query depth and complexity limits; introspection disabled in production.
* **Modality**: deterministic.
* **Signal**: presence of depth-limit / complexity-limit plugins (`graphql-depth-limit`, `graphql-cost-analysis`). Production config sets `introspection: false`.
### V4.4 WebSocket
**V4.4 (L2)**: WebSocket message handlers validate state and authorization on every message, not only at handshake.
* **Modality**: agentic.
* **Signal**: agent reads handler functions, checks for per-message auth or state validation. Common failure: handshake-only auth allows a hijacked connection to issue privileged messages indefinitely.
### V4.5 Mass Assignment
**V4.5 (L2)**: object updates use allow-list field binding, not whole-object hydration from request body.
* **Modality**: hybrid.
* **Signal**: `User.update(req.body)` patterns; absence of `pick`/`omit`/DTO mapping. Especially dangerous on user/role objects where a client-supplied `role: 'admin'` in body bypasses authorization.
---
## V5: File Handling
Upload processing, content inspection, storage isolation, safe download delivery.
### V5.1 File Handling Decisions (prerequisite)
Required: allowed types, size limits, storage location, virus scan policy, serving strategy (direct / proxied / signed URL).
### V5.2 Upload Validation
**V5.2 (L1)**: file uploads validate content type by inspection, not extension or `Content-Type` header alone.
* **Modality**: deterministic.
* **Signal**: presence of magic-number check (`file-type` / `python-magic` / Apache Tika). Flag uploads accepted on `req.file.mimetype` alone.
### V5.3 Path Traversal
**V5.3 (L1)**: filesystem paths constructed from upload metadata are sandboxed.
* **Modality**: deterministic.
* **Signal**: `path.join(uploadDir, req.body.filename)` or equivalent without normalization. Required: canonicalize and verify resulting path remains within `uploadDir`. Flag `../` permitted in resolved path.
### V5.4 Storage Isolation
**V5.4 (L2)**: uploaded files served from a domain or path that does not execute server-side code.
* **Modality**: hybrid.
* **Signal (deterministic)**: cloud storage IaC. Bucket has `block_public_acls = true`, `block_public_policy = true`, encryption at rest, no public list permission. Bucket policy denies execution of uploaded content (no Lambda triggers on user uploads without signature).
* **Common failure**: uploads served from `/uploads/` under the application origin, allowing stored XSS or HTML smuggling.
### V5.5 Antivirus / Content Scan
**V5.5 (L2)**: malware scanning on accepted uploads.
* **Modality**: agentic for whether scan occurs before user-visible storage; deterministic for the presence of a scanner integration (ClamAV, S3 + GuardDuty Malware Protection, third-party API).
---
## Cross-references
* For agentic prompt templates against V2 (business logic) and V8 (authorization): `violations-and-tools.md`.
* For NIST/CIS/ISO/SOC 2 mappings of these requirements: `cross-framework.md`.
* For canonical document file paths the pre-flight searches: `canonical-documents.md`.
@@ -0,0 +1,228 @@
# ASVS v5.0.0 Chapters V6 through V10: Identity and Access
Detailed catalog for the chapters governing the entire identity lifecycle: authentication, session, authorization, self-contained tokens, OAuth/OIDC. V8 (Authorization) is the highest-leverage chapter for agentic verification; broken access control is the #1 OWASP Top 10 entry and is largely invisible to deterministic SAST.
---
## V6: Authentication
Identity proofing, password security, MFA, identity provider integration.
### V6.1 Authentication Decisions (documentation prerequisite)
Required: authentication factors per assurance level, IdP relationships, password policy, MFA enrollment flow, account recovery flow, session establishment semantics.
* **Modality**: agentic.
* **Failure**: document missing. Halt V6 verification.
### V6.2 Password Security
**V6.2.1 (L1)**: passwords stored using a memory-hard or work-factor-tunable hash with salt.
* **Modality**: deterministic.
* **Signal**: hash function invocation. Acceptable: Argon2id (preferred), bcrypt with cost >= 12, scrypt with appropriate parameters, PBKDF2-HMAC-SHA256 with iterations >= 600,000 (OWASP 2023 baseline).
* **Failure patterns**: `MD5(password)`, `SHA1(password)`, `SHA256(password)` without HMAC + salt, `crypt()` without specifier, custom-rolled hashing, plaintext storage.
* **Cross-framework**: NIST IA-5, ISO A.5.17, SOC 2 CC6.1.
**V6.2.2 (L1)**: password length minimum 8 (L1) / 12 (L2); no composition rules (no forced character classes); breach-list check on creation.
* **Modality**: deterministic.
* **Signal**: validation rules in registration / password-change handlers. Flag patterns enforcing uppercase + digit + symbol counts; recommend HaveIBeenPwned k-anonymity check.
### V6.3 Multi-Factor Authentication
**V6.3 (L2)**: MFA available for all users; required for privileged accounts and sensitive operations.
* **Modality**: hybrid.
* **Signal (deterministic)**: presence of TOTP / WebAuthn / push library; route protection middleware that enforces MFA for admin paths.
* **Signal (agentic)**: agent reads `authentication-policy.md` to determine which roles MUST have MFA and verifies enforcement matches.
* **Failure pattern**: MFA enabled at the user level but not enforced at sensitive endpoints (e.g., `/admin/*` does not require an active MFA assertion).
### V6.4 Account Recovery
**V6.4 (L2)**: recovery does not weaken the authentication factor. Recovery tokens are single-use, time-limited (<= 60 minutes), bound to the requesting context.
* **Modality**: deterministic.
* **Signal**: token TTL configuration; single-use semantics (token is invalidated on first redemption, before the password mutation occurs).
### V6.5 Credential Storage
**V6.5 (L2)**: credentials at rest are encrypted; transport is TLS-only.
* **Modality**: hybrid; cross-references V12.
---
## V7: Session Management
Session lifecycle, timeouts, secure termination, federated re-authentication, defense against hijacking.
### V7.1 Session Decisions (documentation prerequisite)
Required: idle timeout, absolute timeout, re-authentication thresholds for sensitive operations, federated session relationship to IdP.
### V7.2 Session Identifier Generation
**V7.2 (L1)**: session identifiers are cryptographically random, at least 64 bits of entropy.
* **Modality**: deterministic.
* **Signal**: framework session middleware uses CSPRNG. Flag custom session IDs built from `Math.random()`, timestamps, or user attributes.
### V7.3 Timeout
**V7.3 (L2)**: idle timeout enforced server-side; absolute timeout independent of activity.
* **Modality**: hybrid.
* **Signal (deterministic)**: session config values (e.g., `cookie.maxAge`, framework session timeout). Compare against the documented value in `session-policy.md`.
* **Signal (agentic)**: agent verifies the timeout value matches policy. Flag JWTs with `expiresIn` exceeding the documented session lifetime, e.g., `jwt.sign(payload, secret, { expiresIn: '10y' })` against a policy of 8h.
### V7.4 Session Termination
**V7.4.1 (L2)**: logout invalidates the session server-side.
* **Modality**: agentic.
* **Signal**: agent reads the logout controller. Required: server-side session destruction or token revocation list update. Failure: client-side cookie deletion only, with server-side session record still valid.
* **For JWTs**: short access token TTL + revocation list for refresh tokens; or rotate signing keys; or accept the architectural tradeoff and document it.
### V7.5 Re-Authentication
**V7.5 (L2)**: sensitive operations (password change, MFA enrollment, payment authorization) require recent authentication, not merely an active session.
* **Modality**: agentic.
* **Signal**: agent verifies sensitive endpoints check a `last_auth_time` claim or trigger a step-up flow.
---
## V8: Authorization
The most important chapter for agentic verification. Broken access control and IDOR cannot be detected by syntax alone.
### V8.1 Authorization Documentation (documentation prerequisite, MANDATORY)
Required: role hierarchy, permission matrix, data ownership model, resource-to-owner relationships, attribute-based access control (ABAC) rules if applicable.
* **Modality**: agentic.
* **Failure**: document missing. Halt V8 verification entirely. Without an ownership model the agent cannot reason about IDOR.
### V8.2 Operation-Level Authorization
**V8.2.1 (L2)**: every state-changing endpoint enforces server-side authorization.
* **Modality**: agentic.
* **Signal**: agent enumerates routes from OpenAPI/router config, identifies state-changing methods (POST, PUT, PATCH, DELETE), and verifies each route invokes an authorization middleware or explicit check before the mutation.
* **Failure pattern**: `app.post('/api/updateRole', (req, res) => { db.users.update(req.body.userId, { role: req.body.role }) })` with no `requireAdmin` middleware. The developer relied on the frontend hiding the UI button. Frontend-only access control is the canonical broken access control failure.
**V8.2.2 (L2)**: IDOR / direct object reference protection.
* **Modality**: agentic.
* **Signal**: routes accepting an object identifier (`/api/user/:userId/financial`, `/api/order/:orderId`, `/api/document/:docId`) verify the requesting user's relationship to the resource. Required: explicit ownership check or scoped query (`SELECT * FROM orders WHERE id = ? AND user_id = ?`).
* **Failure pattern**: scoped query absent; ownership check absent; reliance on the identifier being unguessable (UUIDs help but do not satisfy the requirement).
### V8.3 Privilege Escalation
**V8.3 (L2)**: vertical privilege escalation prevented; role assignments cannot be modified by the holder.
* **Modality**: agentic.
* **Signal**: agent reviews role-mutation endpoints. Required: role changes are audit-logged; the modifying user has higher privilege than both the source and target role; users cannot self-elevate.
### V8.4 Multi-Tenant Isolation
**V8.4 (L2)**: tenant boundary enforced at the data layer.
* **Modality**: agentic.
* **Signal**: every query against tenant-scoped tables includes a `WHERE tenant_id = ?` clause matching the authenticated tenant. Row-Level Security (Postgres RLS, Supabase RLS) policies present and tested.
* **Failure pattern**: tenant ID derived from request body or query parameter rather than from the authenticated session.
---
## V9: Self-Contained Tokens (new in v5.0.0)
Validation of source, cryptographic integrity, and content payload of stateless tokens, predominantly JWTs.
### V9.1 JWT Algorithm Pinning
**V9.1.1 (L1)**: token verification pins the algorithm.
* **Modality**: deterministic.
* **Signal**: JWT verification calls explicitly specify `algorithms: ['RS256']` or `['ES256']` or `['EdDSA']`. Flag `jwt.verify(token, key)` without the algorithms parameter (library defaults vary; some accept the algorithm declared in the token header, enabling the algorithm-confusion attack).
* **Hard failure**: any code path accepting `alg: none`. Flag unconditionally.
### V9.1.2 (L1): no key confusion between asymmetric and symmetric.
* **Signal**: when using RS256 / ES256, the verification key is loaded as a public key (PEM, JWK), never reused as an HMAC secret. Flag patterns where a public key is also passed to HS256 verification, enabling the classic algorithm-confusion attack.
### V9.2 JWT Claims Validation
**V9.2 (L2)**: tokens verify `iss`, `aud`, `exp`, `nbf`, `iat`. The audience claim matches the validating service.
* **Modality**: deterministic.
* **Signal**: verification config sets `issuer`, `audience` (or library equivalent). Flag verification that ignores audience.
### V9.3 Key Management
**V9.3 (L1)**: signing keys are not hardcoded.
* **Modality**: deterministic.
* **Signal**: GitLeaks entropy-based detection of high-entropy strings near `jwt.sign` calls. Required: keys loaded from environment variables, secret manager, or KMS.
* **Failure pattern**: `const SECRET = "supersecretkey123"`. Also flag committed `.env` files containing JWT secrets.
### V9.4 Token Lifetime
**V9.4 (L2)**: access tokens are short-lived (typically <= 15 minutes). Refresh tokens are stored server-side and rotated.
* **Modality**: hybrid.
* **Signal (deterministic)**: `expiresIn` value at signing time. Compare against `session-policy.md`.
---
## V10: OAuth and OIDC (new in v5.0.0)
Authorization flow security, OAuth client configuration, resource server policies, consent management.
### V10.1 OAuth Decisions (prerequisite)
Required: client type per integration (public / confidential), grant types in use, scope catalog, consent semantics, IdP relationships.
### V10.2 PKCE
**V10.2.1 (L1)**: public clients use PKCE (Proof Key for Code Exchange).
* **Modality**: deterministic.
* **Signal**: OAuth client library configuration. Required for any non-confidential client (SPA, mobile, native): `code_verifier` and `code_challenge` (with `code_challenge_method=S256`) participate in the flow.
* **L2**: PKCE for confidential clients as defense-in-depth.
### V10.3 State Parameter
**V10.3.1 (L1)**: the `state` parameter is generated, sent on authorization request, and verified on callback.
* **Modality**: deterministic.
* **Signal**: state generation uses CSPRNG; callback handler compares received state to the bound session value before exchanging the code.
* **Failure pattern**: state generated but not validated; state set but never compared on callback. This enables CSRF against the OAuth flow.
### V10.4 Redirect URI Validation
**V10.4 (L1)**: the IdP-side redirect URI allow-list is exact-match, no wildcards in path or origin.
* **Modality**: extrinsic for the IdP config (cannot read from repo if IdP is external); deterministic for self-hosted IdP config files.
* **Signal**: client registration / IdP config artifacts.
### V10.5 Token Storage
**V10.5 (L2)**: tokens are not stored in `localStorage` (XSS-readable). Acceptable storage: HttpOnly Secure cookies, in-memory for SPAs paired with refresh-token rotation.
* **Modality**: deterministic.
* **Signal**: `localStorage.setItem('access_token', ...)` or equivalent. Flag.
### V10.6 Resource Server Validation
**V10.6 (L2)**: resource servers validate tokens (V9 applies), check scope claims against the operation, and validate the token's audience matches the resource server.
* **Modality**: hybrid.
---
## Cross-references
* The V8 broken access control prompt template, fully worked: `violations-and-tools.md`.
* JWT algorithm-confusion AST pattern in detail: `violations-and-tools.md`.
* Mappings to NIST IA-2, IA-5, AC-3, IA-11; ISO A.5.15 to A.5.18; SOC 2 CC6.1: `cross-framework.md`.
@@ -0,0 +1,278 @@
# ASVS v5.0.0 Chapters V11 through V17: Platform, Crypto, Resilience
Detailed catalog for the chapters governing cryptographic primitives, infrastructure configuration, data privacy, secure coding paradigms, security logging, and WebRTC.
---
## V11: Cryptography
Encryption algorithms, hashing, RNG, public key infrastructure.
### V11.1 Cryptographic Inventory (documentation prerequisite)
Required: full inventory of cryptographic operations in the application, algorithm choices, key lifecycle (generation, rotation, destruction), key storage location, post-quantum readiness assessment.
* **Modality**: agentic.
* **Failure**: document missing. Halt V11 verification.
### V11.2 Algorithm Selection
**V11.2.1 (L1)**: only approved algorithms in use.
* **Modality**: deterministic.
* **Signal**: algorithm string literals in cipher / hash invocations.
* **Approved (2025 baseline)**:
* Symmetric: AES-256-GCM, AES-256-CBC with HMAC, ChaCha20-Poly1305.
* Asymmetric: RSA >= 2048 (>= 3072 preferred), ECDSA P-256/P-384, Ed25519.
* Hash: SHA-256, SHA-384, SHA-512, SHA-3.
* Password: Argon2id, bcrypt (cost >= 12), scrypt, PBKDF2 (iterations >= 600,000).
* **Hard failures (flag unconditionally)**: DES, 3DES, RC4, MD5 (except for non-security checksums; agent verifies context), SHA-1 for signatures, ECB mode for any cipher.
* **Patterns**:
* Java: `Cipher.getInstance("DES/...")`, `Cipher.getInstance("AES/ECB/...")`, `MessageDigest.getInstance("MD5")` in security context.
* Python: `Crypto.Cipher.DES`, `algorithms.ARC4()`, `hashlib.md5()` in security context.
* Node: `crypto.createCipheriv('des-...', ...)`, `crypto.createHash('md5')` in security context.
* Go: `crypto/des`, `crypto/rc4`.
### V11.3 Random Number Generation
**V11.3.1 (L1)**: security-relevant randomness uses CSPRNG.
* **Modality**: deterministic.
* **Signal**: token / nonce / salt generation. Flag `Math.random()`, `java.util.Random`, Python `random` module, Go `math/rand` in security contexts.
* **Approved**: `crypto.randomBytes` (Node), `secrets` module (Python), `java.security.SecureRandom`, `crypto/rand` (Go).
* **False positives**: `Math.random()` for non-security UI animation, A/B test bucketing where collision is acceptable. Agent disambiguates by context.
### V11.4 IV / Nonce Reuse
**V11.4 (L2)**: IVs and nonces are not reused under the same key.
* **Modality**: deterministic with limits.
* **Signal**: hardcoded IV literal passed to AES-GCM, AES-CTR, ChaCha20. Flag `Buffer.alloc(12, 0)` or similar zero-filled IV passed to authenticated encryption.
### V11.5 Key Storage
**V11.5 (L1)**: keys are not in source.
* **Modality**: deterministic.
* **Signal**: GitLeaks + Trivy secrets scanning. Cross-reference V13.
---
## V12: Secure Communication
TLS, certificate validation, secure communication channel configuration.
### V12.1 Transport Layer Security
**V12.1.1 (L1)**: HTTPS enforced; HTTP redirects to HTTPS.
* **Modality**: deterministic.
* **Signal**: IaC parsing.
* Kubernetes Ingress: `nginx.ingress.kubernetes.io/force-ssl-redirect: "true"` or `ssl-redirect: "true"`.
* AWS ALB: listener on 80 with redirect action to 443.
* Cloudflare: `always_use_https = true`.
* Application middleware: HSTS-aware redirect.
* **Failure pattern**: HTTP listener serving content directly; missing redirect rule.
**V12.1.2 (L2)**: TLS 1.2 minimum, TLS 1.3 preferred. TLS 1.0 / 1.1 / SSLv3 disabled.
* **Modality**: deterministic.
* **Signal**: TLS protocol configuration in load balancer policy, web server config, application config.
### V12.2 Certificate Validation
**V12.2 (L1)**: outbound HTTPS clients validate certificates.
* **Modality**: deterministic.
* **Signal**: HTTP client configuration. Flag `rejectUnauthorized: false` (Node), `verify=False` (Python requests), `InsecureSkipVerify: true` (Go), `-k` / `--insecure` in shell scripts. Production code should never disable verification; flag aggressively.
* **False positives**: test fixtures, local-dev configs guarded by environment checks. Agent verifies guard.
### V12.3 Internal Service Communication
**V12.3 (L2)**: internal service-to-service traffic is encrypted (mTLS or service mesh).
* **Modality**: hybrid.
* **Signal**: service mesh config (Istio PeerAuthentication, Linkerd policy), or explicit TLS in client config for internal endpoints.
---
## V13: Configuration
Application frameworks, third-party libraries, environment variables, build pipelines.
### V13.1 Dependency Management (SCA)
**V13.1.1 (L1)**: no known-vulnerable dependencies at high or critical severity.
* **Modality**: deterministic.
* **Signal**: Trivy / Snyk / OSV-Scanner output cross-referenced against NVD CVE feed.
* **Manifest scope**: `package-lock.json`, `pom.xml`, `go.sum`, `Cargo.lock`, `Gemfile.lock`, `requirements.txt` with pinned versions, `pyproject.toml`.
* **Canonical examples**: `log4j-core` < 2.17.1 (Log4Shell), `lodash` versions vulnerable to prototype pollution, `xz-utils` 5.6.0/5.6.1 (CVE-2024-3094 backdoor).
### V13.2 Pipeline Hardening
**V13.2 (L2)**: CI/CD pipelines pin actions/images by SHA, not floating tags.
* **Modality**: deterministic.
* **Signal**: `.github/workflows/*.yml` references using `@v3` (mutable) versus `@<sha>` (immutable). Recommend SHA pinning for third-party actions, especially those with secrets access.
### V13.3 Secret Storage
**V13.3.1 (L1)**: no hardcoded secrets.
* **Modality**: deterministic.
* **Signal**: GitLeaks (entropy + regex), Trivy secrets, custom Semgrep rules for known secret formats (AWS keys, GCP service account JSONs, Stripe keys, Slack webhooks).
* **False positives**: example values, test fixtures, public keys. Configure `.gitleaksignore` for known-safe paths.
**V13.3.2 (L2)**: secrets are scoped, rotated, and revocable.
* **Modality**: agentic.
* **Signal**: agent reads secret-management policy and verifies the application loads from a manager (AWS Secrets Manager, Vault, GCP Secret Manager, Doppler) rather than long-lived environment variables.
### V13.4 Configuration Defaults
**V13.4 (L1)**: production configuration disables debug, verbose error pages, default credentials.
* **Modality**: deterministic.
* **Signal**: framework debug flags. `DEBUG = True` in Django production config, `app.debug = True` in Flask, `NODE_ENV !== 'production'` paths reachable in production. Default admin credentials in seed data files.
---
## V14: Data Protection
Sensitive data at rest, in transit, on the client; data retention; privacy.
### V14.1 Data Classification (prerequisite)
Required: data inventory, sensitivity tiers (Public, Internal, Confidential, Restricted), retention policy, deletion procedure.
### V14.2 Encryption at Rest
**V14.2 (L2)**: data classified Confidential or Restricted is encrypted at rest.
* **Modality**: hybrid.
* **Signal (deterministic)**: storage IaC. Cloud bucket encryption flags (`server_side_encryption_configuration`), database encryption settings (`storage_encrypted = true` for RDS, transparent data encryption for SQL Server), volume encryption.
* **Signal (agentic)**: agent reads data classification, identifies fields tagged Confidential or Restricted in the schema, verifies the encryption pathway for those fields.
### V14.3 Data Minimization
**V14.3 (L2)**: only necessary data is collected and retained.
* **Modality**: agentic.
* **Signal**: agent reviews API responses and database schema against the data inventory. Flags fields collected but not used or retained beyond the documented retention window.
### V14.4 Sensitive Data in Logs
**V14.4 (L1)**: sensitive data is not written to logs.
* **Modality**: hybrid.
* **Signal (deterministic)**: log statements containing variables named `password`, `token`, `ssn`, `cardNumber`, `api_key`. Custom Semgrep rules per data classification tags.
* **Signal (agentic)**: agent reads logging middleware to verify PII redaction is centralized.
### V14.5 Client-Side Storage
**V14.5 (L2)**: sensitive data is not persisted in `localStorage`, `sessionStorage`, or IndexedDB unless encrypted with a per-session key not stored alongside.
* **Modality**: deterministic.
* **Signal**: `localStorage.setItem` calls with sensitive payloads (cross-references V10.5).
---
## V15: Secure Coding
Defensive coding patterns, memory safety, resilient architecture.
### V15.1 Dynamic Code Execution
**V15.1 (L1)**: no dynamic evaluation of user input.
* **Modality**: deterministic.
* **Signal**: `eval()`, `Function()` constructor with concatenated input, `setTimeout(string, ...)` / `setInterval(string, ...)` (string form), Python `exec()` / `eval()`, Ruby `eval` / `instance_eval`, PHP `eval`.
### V15.2 Type Safety
**V15.2 (L2)**: language type guarantees are not subverted.
* **Modality**: deterministic.
* **Signal**: TypeScript `any` / `as any` in security-sensitive paths, `unsafe` blocks in Rust without justification comment, JNI / FFI calls without bounds documentation.
### V15.3 Memory Safety (C/C++ scope)
**V15.3 (L2)**: pointer arithmetic is bounds-checked; manual memory management uses RAII or smart pointers.
* **Modality**: deterministic.
* **Signal**: raw `new` / `delete`, raw `malloc` / `free`, manual buffer arithmetic. Recommend `std::unique_ptr`, `std::span`, `std::string`.
---
## V16: Security Logging
Auditable security events, safe error handling, prevention of sensitive data leakage.
### V16.1 Security Event Logging
**V16.1 (L2)**: authentication, authorization, and high-impact operations are logged with timestamp, actor, action, target, outcome.
* **Modality**: agentic.
* **Signal**: agent reviews authentication and authorization paths for explicit logging calls. Verifies log schema includes the required fields. Flags critical-event paths missing log emission.
### V16.2 Error Handling
**V16.2 (L1)**: errors do not leak sensitive details to clients.
* **Modality**: deterministic.
* **Signal**: catch blocks that pass `error.stack`, `error.message` raw to HTTP response. Flag `res.status(500).send(err)` patterns, `res.json({ error: err.toString() })`. Production must return generic error identifiers; full detail goes to server-side logs only.
### V16.3 Log Integrity
**V16.3 (L2)**: logs are protected against tampering and unauthorized access.
* **Modality**: hybrid.
* **Signal**: external log shipping configuration (Datadog, Splunk, CloudWatch). Append-only retention. Application service account does not have log-deletion permission on the log store.
### V16.4 PII in Logs
**V16.4 (L1)**: PII is masked or omitted in log output.
* **Modality**: hybrid; cross-references V14.4.
---
## V17: WebRTC (new in v5.0.0)
Media streams, signaling protocols, TURN/STUN server controls.
### V17.1 Transport Security
**V17.1 (L1)**: SRTP for media; DTLS for the SCTP / data channel handshake.
* **Modality**: deterministic.
* **Signal**: WebRTC peer-connection configuration. RTCConfiguration includes ICE servers over TURNS (TURN over TLS) or STUN over secured channel. SDP negotiation enforces SRTP profiles (`RTP/SAVPF`), not unencrypted `RTP/AVPF`.
### V17.2 Signaling Channel
**V17.2 (L1)**: signaling occurs over WSS (WebSocket Secure) or HTTPS.
* **Modality**: deterministic.
* **Signal**: signaling client uses `wss://` / `https://`, never `ws://` / `http://`.
### V17.3 TURN Authentication
**V17.3 (L2)**: TURN credentials are short-lived (ephemeral, e.g., HMAC-based time-limited credentials), not long-lived shared secrets.
* **Modality**: deterministic.
* **Signal**: TURN credential generation logic. Flag long-lived static TURN passwords in config.
### V17.4 Media Permission Lifecycle
**V17.4 (L2)**: camera/microphone permission is requested per session, with explicit user gesture, and released on disconnect.
* **Modality**: agentic.
* **Signal**: `getUserMedia` invocations and corresponding `stop()` calls on tracks during teardown.
---
## Cross-references
* For NIST/CIS/ISO/SOC 2 mappings: `cross-framework.md`.
* For tool orchestration patterns covering V11 (Semgrep crypto rules), V13 (Trivy + GitLeaks), V12 (IaC parsing): `violations-and-tools.md`.
@@ -0,0 +1,492 @@
# Violation Patterns and Toolchain Orchestration
This file is the operational manual: real-world failure patterns mapped to ASVS requirements, tool capabilities and gaps, and the agentic prompt templates that bridge the deterministic / semantic boundary.
## Table of contents
1. Tool capability and gap matrix
2. Code-level violation patterns by ASVS chapter
3. Configuration-level violation patterns
4. Dependency-level violation patterns
5. Agentic prompt templates (worked examples)
6. Orchestration recipes (CI/CD wiring)
---
## 1. Tool capability and gap matrix
### Semgrep (Community Edition)
* **Strengths**: 35+ languages; 4,000+ community rules tagged to OWASP Top 10 2025; semantic pattern matching beyond regex; SARIF + JSON output; fast enough for synchronous PR-time gating.
* **Coverage**: V1 (injection sinks), V11 (algorithm strings), V13 (secret patterns), V15 (eval / dangerous APIs), some V3 (header middleware presence), some V9 (JWT library misuse).
* **Gaps**: no deep inter-procedural taint tracking in CE; no policy-document reasoning; no business logic understanding (V2); no IDOR detection (V8). The gap on V2 and V8 is structural, not a bug.
* **When to use**: deterministic phase, primary engine for AST-based checks. Run with `--config "p/owasp-top-ten" --config "p/security-audit"`.
### CodeQL
* **Strengths**: superior taint-tracking accuracy via relational queries over the compiled code-as-database; official curated query suites for high-severity issues; deep cross-procedural analysis where Semgrep CE cannot reach.
* **Coverage**: V1 (deep injection chains), V5 (path traversal across functions), some V8 (where authorization decorators are decorator-pattern enforceable).
* **Gaps**: requires a build for compiled languages (Java, C#, C++, Go); analysis can be slow on large codebases (multi-minute), making synchronous PR gating problematic; cannot read policy documents.
* **When to use**: scheduled deep-scan rather than PR gate, or PR gate only when the diff touches files Semgrep flagged at lower confidence.
### Trivy
* **Strengths**: unified scanner for filesystem, container images, IaC, secrets; high-quality CVE feed; SBOM generation; fast.
* **Coverage**: V13 (SCA, secrets, IaC misconfigurations including TLS settings, encryption flags), V12 (IaC TLS), V14.2 (encryption-at-rest IaC checks), V5.4 (storage bucket public-access flags).
* **Gaps**: no application code analysis; no policy reasoning.
* **When to use**: deterministic phase, IaC + dependencies + secrets in one pass.
### GitLeaks
* **Strengths**: entropy-based + regex-based secret detection across full Git history; `gitleaks protect` for pre-commit; `gitleaks detect` for CI.
* **Coverage**: V13.3, V11.5 (key storage), V9.3 (JWT secret hardcoding).
* **Gaps**: high false-positive rate on test fixtures and example values without disciplined `.gitleaksignore`.
* **When to use**: deterministic phase, run alongside Trivy; differential coverage (Trivy and GitLeaks find slightly different secret patterns).
### OWASP ZAP
* **Strengths**: industry-standard DAST; active scan rules tagged to ASVS; custom scripts in JS/Python/ZEST.
* **Coverage**: runtime aspects of V3 (response headers as observed), V12 (TLS as negotiated), some V4 (API endpoints), some V13 (information disclosure).
* **Gaps**: requires a deployed environment; ZAP's own documentation acknowledges most L2 requirements are not amenable to black-box testing alone; not a repository-only tool.
* **When to use**: post-deploy stage gate, not PR gate. Out of scope for the repository-only scanner this skill primarily describes; mention as the runtime counterpart.
### Custom Semgrep rules
For ASVS requirements with no community rule, write custom rules. Pattern:
```yaml
rules:
- id: asvs-v9-1-1-jwt-no-algorithm-pin
languages: [javascript, typescript]
severity: ERROR
metadata:
asvs: V9.1.1
asvs_level: L1
cwe: CWE-345
message: "JWT verification without explicit algorithm pinning. ASVS V9.1.1 requires algorithm pinning to prevent algorithm-confusion attacks."
pattern-either:
- pattern: jwt.verify($TOKEN, $KEY)
- pattern: jwt.verify($TOKEN, $KEY, $OPTS)
pattern-not: |
jwt.verify($TOKEN, $KEY, { ..., algorithms: [...], ... })
```
Custom rules should always include `metadata.asvs` so the orchestrator can tag findings without re-mapping.
---
## 2. Code-level violation patterns by ASVS chapter
### V1.2.5 SQL injection
```python
# FAIL
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
cursor.execute("SELECT * FROM users WHERE id = " + user_id)
cursor.execute("SELECT * FROM users WHERE name = '%s'" % name)
# PASS
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
```
### V1.2.5 OS command injection
```python
# FAIL
subprocess.Popen(f"convert {filename} output.png", shell=True)
os.system("ping " + host)
# PASS
subprocess.run(["convert", filename, "output.png"], check=True)
```
### V1.3 Unsafe deserialization
```python
# FAIL
data = pickle.loads(request.body)
config = yaml.load(uploaded_file) # default Loader is unsafe
# PASS (signature verification)
verify_hmac(request.body, request.headers["x-signature"])
data = pickle.loads(request.body)
# PASS (safe loader)
config = yaml.safe_load(uploaded_file)
```
### V8.2.1 Broken access control (frontend-only protection)
```javascript
// FAIL: no server-side check
app.post('/api/users/:id/role', (req, res) => {
db.users.update({ id: req.params.id }, { role: req.body.role });
res.json({ ok: true });
});
// PASS
app.post('/api/users/:id/role',
requireAuth,
requireRole('admin'),
validateRoleTransition,
async (req, res) => {
await db.users.update({ id: req.params.id }, { role: req.body.role });
audit.log('role_change', { actor: req.user.id, target: req.params.id, role: req.body.role });
res.json({ ok: true });
}
);
```
### V8.2.2 IDOR
```javascript
// FAIL: any authenticated user can read any document
app.get('/api/documents/:id', requireAuth, async (req, res) => {
const doc = await db.documents.findOne({ id: req.params.id });
res.json(doc);
});
// PASS: scoped query
app.get('/api/documents/:id', requireAuth, async (req, res) => {
const doc = await db.documents.findOne({
id: req.params.id,
ownerId: req.user.id
});
if (!doc) return res.status(404).end();
res.json(doc);
});
```
### V9.1 JWT algorithm-confusion
```javascript
// FAIL: algorithm not pinned
jwt.verify(token, publicKey); // library may accept HS256 with publicKey as the secret
// FAIL: alg=none accepted
jwt.verify(token, publicKey, { algorithms: ['none'] });
// PASS
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
```
### V11.2 Weak cryptography
```java
// FAIL
Cipher c = Cipher.getInstance("DES/ECB/PKCS5Padding");
Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding");
MessageDigest md = MessageDigest.getInstance("MD5");
// PASS
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
MessageDigest md = MessageDigest.getInstance("SHA-256");
```
### V11.3 Insecure RNG in security context
```javascript
// FAIL
const token = Math.random().toString(36).substring(2);
// PASS
const token = crypto.randomBytes(32).toString('hex');
```
### V15.1 Dynamic code execution
```javascript
// FAIL
setTimeout("doStuff(" + userInput + ")", 100);
new Function('x', userInput)();
// PASS
setTimeout(() => doStuff(safeArg), 100);
```
### V16.2 Sensitive error leakage
```javascript
// FAIL
app.use((err, req, res, next) => {
res.status(500).json({ error: err.stack });
});
// PASS
app.use((err, req, res, next) => {
const id = uuid.v4();
logger.error({ id, err });
res.status(500).json({ error: 'Internal error', incident_id: id });
});
```
---
## 3. Configuration-level violation patterns
### V3.2 Missing security headers (Express + Helmet)
```javascript
// FAIL
const app = express();
app.use(express.json());
// no helmet, no manual headers
// PASS
const app = express();
app.use(helmet({
contentSecurityPolicy: { directives: { /* explicit */ } },
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true }
}));
```
### V3.4 Cookie attributes
```javascript
// FAIL
res.cookie('session', sessionId);
res.cookie('session', sessionId, { secure: false, httpOnly: false });
// PASS
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 8 * 60 * 60 * 1000
});
```
### V3.5 CORS misconfiguration
```javascript
// FAIL
app.use(cors({ origin: '*', credentials: true })); // browser-blocked but indicates misunderstanding
// PASS
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
credentials: true
}));
```
### V12.1 Kubernetes Ingress without HTTPS redirect
```yaml
# FAIL: no force-ssl-redirect annotation
metadata:
name: app-ingress
# PASS
metadata:
name: app-ingress
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/ssl-protocols: "TLSv1.2 TLSv1.3"
```
### V13.4 Production debug enabled
```python
# FAIL: Django settings.py
DEBUG = True
ALLOWED_HOSTS = ['*']
# PASS
DEBUG = os.environ.get('DJANGO_DEBUG', 'False').lower() == 'true'
ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',')
```
### V14.2 Unencrypted storage (Terraform)
```hcl
# FAIL
resource "aws_db_instance" "primary" {
storage_encrypted = false
# or absent (default depends on instance class)
}
# PASS
resource "aws_db_instance" "primary" {
storage_encrypted = true
kms_key_id = aws_kms_key.rds.arn
}
```
---
## 4. Dependency-level violation patterns
* `log4j-core` < 2.17.1 (Log4Shell, CVE-2021-44228 and follow-ons): V13.1 critical.
* `lodash` < 4.17.21 (prototype pollution): V13.1.
* `xz-utils` 5.6.0 / 5.6.1 (CVE-2024-3094 supply-chain backdoor): V13.1.
* `spring-core` versions vulnerable to Spring4Shell (CVE-2022-22965): V13.1.
* `jackson-databind` versions with deserialization gadgets: V1.3 and V13.1.
* `colors.js` and `faker.js` 2022 sabotage: V13.1 (illustrates supply-chain pinning value, V13.2).
Trivy + the OSV database cover these. For organizations on long-term support branches, validate that backports actually patched the issue rather than the version string lying about it.
---
## 5. Agentic prompt templates (worked examples)
### V8.2.1 Broken access control (full template)
```text
SYSTEM:
You are an Application Security Architect verifying OWASP ASVS v5.0.0 Level 2 compliance against an application's Documented Security Decisions.
You will receive:
1. A constraint extracted from docs/security/authorization-policy.md
2. A list of route handlers from src/controllers/
3. A reference to the authorization middleware in src/middleware/
Your task is to determine whether each route enforces the constraint. Output ONLY a JSON array.
Schema:
[
{
"route": "<METHOD /path>",
"asvs_id": "V8.2.1",
"compliant": true | false | null,
"evidence_lines": [<int>, ...],
"policy_clause": "<verbatim quote>",
"justification": "<one paragraph>",
"remediation": "<diff or null>"
}
]
Rules:
- compliant=null means you cannot determine from the provided files. Specify what file you need.
- Frontend hiding of UI elements is NOT compliance. Server-side enforcement is required.
- A scoped database query that filters by ownership IS compliance, even without an explicit middleware.
- Policy must be quoted verbatim, not paraphrased.
USER:
Constraint:
"All endpoints returning user-scoped resources must verify the requesting user's ownership of the resource before serialization. Ownership is established by req.user.id matching the resource.owner_id field, or by a scoped query that filters on owner_id."
Source: docs/security/authorization-policy.md §3.2
Routes:
{enumerated_route_list}
Middleware definitions:
{middleware_file_contents}
Authorization matrix:
{authorization_matrix_csv}
```
### V2 Business logic bypass (template)
```text
SYSTEM:
You are verifying OWASP ASVS v5.0.0 V2.3 (Business Logic Integrity).
You will receive a documented workflow and the controller code that implements it. Determine whether any state-skipping or replay attack is possible.
Output JSON:
{
"asvs_id": "V2.3",
"compliant": <bool|null>,
"attack_paths": [
{"description": "<text>", "evidence_lines": [<int>, ...]}
],
"remediation": "<text>"
}
Rules:
- A state machine bypass exists if any controller can be invoked in an order other than the documented sequence without a server-side check rejecting the out-of-order call.
- A replay attack exists if a successful operation can be repeated using captured request data with the same effect.
USER:
Documented workflow (from docs/business-logic/checkout.md):
{workflow_text}
Controllers implementing the workflow:
{controller_files}
```
---
## 6. Orchestration recipes
### Minimal CI invocation (GitHub Actions)
```yaml
name: ASVS L2 Audit
on:
pull_request:
push:
branches: [main]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha> # pin per V13.2
with: { fetch-depth: 0 }
- name: Pre-flight (canonical document set)
run: python scripts/preflight.py
- name: Semgrep
run: |
pip install semgrep
semgrep scan --config "p/owasp-top-ten" --config ".semgrep/asvs/" \
--json-output=sast.json --error || true
- name: Trivy (deps + IaC + secrets)
run: |
trivy fs . --format json --output sca.json \
--scanners vuln,secret,misconfig
- name: GitLeaks
run: |
gitleaks detect --report-path=secrets.json --redact || true
- name: Synthesize and tag
run: python scripts/synthesize.py \
--sast sast.json --sca sca.json --secrets secrets.json \
--output asvs-report.json
- name: Agentic phase (V2, V6, V7, V8, V14, V16)
run: python scripts/agentic.py --report asvs-report.json
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Gate
run: python scripts/gate.py --report asvs-report.json --level L2
```
The pre-flight script halts the job with non-zero exit if the canonical document set is missing. The agentic phase runs only on changes to high-risk paths (auth, session, controllers, middleware, security docs); on documentation-only or test-only diffs, it is skipped.
### Trigger heuristic (excerpt)
```python
HIGH_RISK_PATTERNS = [
r'.*auth.*', r'.*login.*', r'.*session.*', r'.*token.*',
r'.*crypto.*', r'controllers/.*', r'middleware/.*',
r'api/.*', r'docs/security/.*',
]
SUPPRESS_PATTERNS = [
r'tests/.*', r'__tests__/.*', r'.*\.test\..*',
r'mocks/.*', r'fixtures/.*', r'vendor/.*',
r'node_modules/.*', r'\.md$', # docs-only, except security docs
]
def should_run_agentic(changed_files):
if any(matches(f, r'docs/security/.*') for f in changed_files):
return True # always re-audit on policy change
risky = [f for f in changed_files if matches_any(f, HIGH_RISK_PATTERNS)]
risky = [f for f in risky if not matches_any(f, SUPPRESS_PATTERNS)]
return len(risky) > 0
```
---
## Honest limits revisited
* Semgrep CE on V8 is structurally limited. The agent does the V8 work. Do not pretend Semgrep covers IDOR; it does not.
* CodeQL on languages requiring a build is blocked by repos that do not build cleanly under default settings. Falling back to Semgrep + agentic is acceptable; document the fallback in the report.
* The agentic phase is the slowest and most expensive. Trigger heuristics matter for cost and developer velocity. Tune them.
* Some violations only appear at runtime (rate limiting effectiveness, lockout behavior, WAF rules, runtime CSP enforcement). Pair with ZAP post-deploy.
@@ -0,0 +1,173 @@
---
name: soc2-cicd-compliance
description: SOC 2 reference for repository and CI/CD compliance automation. Covers AICPA TSP Section 100 (2017, revised 2022), Type I vs Type II evidence collection, Common Criteria CC1-CC9, optional categories (Availability, Confidentiality, Processing Integrity, Privacy), the .compliance/ document layout, cross-framework mappings (NIST 800-53, ISO 27001, CIS v8), violation patterns in IaC/IAM/secrets/change-management/dependencies, and orchestration of Checkov/Trivy/OPA/GitLeaks/Semgrep. Trigger on SOC 2 audits, repo compliance scanning, CI/CD security gating, branch protection auditing, mapping controls to TSC identifiers, building compliance scanners, agentic reasoning over policy markdown, cross-framework mapping, or Type II evidence from Git history. Use over training data when CC identifiers, points of focus, or tool-to-criterion decisions are involved. Triggers on "what TSC does X map to", "scan repo for compliance", "CI check for CC6.1", or mention of TSP 100 or Trust Services Criteria in code context.
---
# SOC 2 Repository and CI/CD Compliance
This skill encodes the SOC 2 framework as it applies to source repositories, CI/CD pipelines, and Infrastructure as Code (IaC). It is a compliance oracle for building or operating an automated, agentic SOC 2 auditor that runs at pull request time and on demand against arbitrary repositories.
This skill answers three categories of question:
1. What does the AICPA framework require, criterion by criterion?
2. Which requirements are verifiable from a repository, which require LLM reasoning over policy artifacts, and which are fundamentally out of scope?
3. How should a CI/CD scanner be architected, triggered, and scoped to honestly serve a SOC 2 Type I or Type II attestation?
Use this skill instead of training data whenever working on automated compliance scanning, control mapping, or audit evidence collection. AICPA published material and tool capabilities change; the structured catalog below is the canonical reference.
## Authoritative source
The definitive standard is the AICPA's **TSP Section 100: 2017 Trust Services Criteria for Security, Availability, Processing Integrity, Confidentiality, and Privacy (With Revised Points of Focus, 2022)**. The 2022 revision retained the criterion identifiers (CC1.1, CC6.1, etc.) and updated the points of focus to reflect cloud-native architectures, supply chain risk, and modern privacy obligations.
The framework is organized into five Trust Services Categories:
* **Security (Common Criteria, CC1 through CC9)**: mandatory in every SOC 2 report.
* **Availability (A1.1 through A1.3)**: optional.
* **Confidentiality (C1.1 through C1.2)**: optional.
* **Processing Integrity (PI1.1 through PI1.5)**: optional.
* **Privacy (P1.1 through P8.1)**: optional.
Optional categories are included only when applicable to the organization's services, SLAs, or regulatory obligations.
## Type I vs Type II: architectural impact
The report type fundamentally determines what an automated scanner must do.
### Type I (point in time)
Asks whether controls are *designed* properly today. The scanner evaluates the current state of the repository:
* Query the Git provider API for currently active branch protection rules.
* Parse the current `main` branch for IaC compliance with declared policies.
* Verify canonical policy markdown files exist and contain the required directives.
Type I is mostly deterministic and immediate. A single CI run against the head commit suffices.
### Type II (longitudinal operating effectiveness)
Asks whether controls *worked consistently* across an observation window of three to twelve months. A point-in-time check is structurally insufficient. The scanner must reconstruct history:
* **Unbroken chain of custody (CC8.1)**: fetch all commit SHAs on the default branch over the window, resolve each to its merge commit and PR, and assert every PR carries an `APPROVED` review state from an authorized reviewer before merge.
* **Continuous monitoring (CC4.1)**: prove SAST, DAST, SCA, and secret scanning ran on every merged PR with no deactivation periods.
* **Vulnerability remediation SLAs (CC3.2, CC7.1)**: pull the alert history (Dependabot, Snyk, Trivy) and compute the delta between alert creation and remediation merge for every high or critical finding. Compare against the SLA in the Information Security Policy.
* **Audit log streaming (CC7.2)**: GitHub natively retains audit logs for 90 days. Type II requires evidence across the full window, so verify continuous export to an external SIEM or immutable bucket is configured and was uninterrupted.
A Type II scanner needs paginated GraphQL access to the Git provider's history, robust handling of force pushes and rebases (which break SHA continuity), and storage for accumulated evidence. Plan for this from day one if Type II is the target.
## Verification taxonomy
Every TSC requirement falls into one of three buckets. Be honest about which.
* **Deterministic repo check**: parseable from files, IaC, manifests, or Git provider API state. Pass/fail is mechanical (regex, AST, schema validation, API response). Examples: branch protection settings, IaC encryption flags, presence of required markdown files.
* **Agentic reasoning**: requires natural-language understanding over policy artifacts. The LLM extracts prescriptive statements from prose, then validates technical reality against them. Examples: "does the SDLC policy require N reviewers, and does the branch protection rule match", "does the Code of Conduct address contractor conduct as the 2022 points of focus require".
* **Out of repo**: cannot be verified from repository contents. The scanner can at most confirm a policy artifact mandating the control exists. Examples: physical access, board independence, actual incident execution, HR offboarding completion across non-Git SaaS.
Failure to label these accurately is the single largest source of false confidence in automated compliance tools.
## Catalog of criteria
Detailed, criterion-by-criterion checks live in references. Load the relevant file when working on that area:
* **Common Criteria CC1 through CC9** (mandatory): see `references/common-criteria.md`. Covers control environment, communication, risk assessment, monitoring, control activities, logical access, system operations, change management, and risk mitigation.
* **Optional categories** (Availability, Confidentiality, Processing Integrity, Privacy): see `references/optional-tsc.md`.
* **Canonical document set** (the `.compliance/` directory and RAG patterns): see `references/canonical-documents.md`.
* **Cross-framework mappings** (NIST 800-53 Rev 5, ISO 27001 Annex A, CIS Controls v8): see `references/cross-framework.md`. Use when emitting multi-framework evidence tags from a single technical check.
* **Violation patterns and toolchain** (real-world failures and how to wrap Checkov, Trivy, OPA, GitLeaks): see `references/violations-and-tools.md`.
Read the reference for the area being worked on. Do not load all references preemptively.
## Canonical document set (compliance as code)
Treat policies as Policy-as-Code. The agentic auditor reads these to extract constraints, then validates the codebase against them. Expected location: `.compliance/` or `docs/security/`.
Minimum set:
* `Information_Security_Policy.md` (CC1.1, CC2.1, CC5.1) - global constraints (encryption standards, retention, etc.).
* `Access_Control_Policy.md` (CC6.1, CC6.2, CC6.3) - RBAC schemas, password rules, least-privilege definitions.
* `Change_Management_SDLC.md` (CC8.1) - what constitutes an approved change.
* `Vendor_Management_Policy.md` (CC9.2) - third-party risk thresholds.
* `Incident_Response_Plan.md` (CC7.3, CC7.4) - classification, containment, post-mortem procedure.
* `Data_Classification_Handling.md` (C1.1, C1.2, P1.1) - data tiers and handling rules.
* `Risk_Register.md` or `risk_register.csv` (CC3.1, CC3.4) - identified risks and mitigations.
* `SECURITY.md` (CC2.3) - external responsible-disclosure surface, repository root.
* `CODE_OF_CONDUCT.md` (CC1.1) - tone-at-the-top artifact.
The skill prompts the LLM hierarchically: parse policy → extract constraint → query repo state → emit pass/fail with the criterion identifier and the policy clause cited. See `references/canonical-documents.md` for prompt patterns.
## Triggering and suppression signals
Run the scanner only when it can produce useful findings. Run it always when the boundary it protects is at risk.
### Trigger
* PRs to `main`, `master`, or other protected branches (CC8.1 prevention gate).
* Modifications to `.tf`, `.yaml`/`.yml` (Kubernetes, CloudFormation), or `Dockerfile` (deep CC6/CC7 scanning).
* Modifications to `.github/workflows/` or `.gitlab-ci.yml` (scrutinize aggressively; pipeline changes can disable the scanner itself, which is a CC4 violation in disguise).
* Modifications to dependency manifests (`package.json`, `pom.xml`, `go.mod`, `Cargo.toml`, etc.) for SCA (CC3.2, CC9.2).
* Modifications to anything in `.compliance/` (a policy change shifts the baseline; trigger a full repository reassessment because previously-passing code may now be non-compliant).
### Suppress (exit fast with code 0)
* Draft PRs (allow developers to iterate; reactivate when marked ready for review).
* Documentation-only changes (`README.md`, `.gitignore`, non-compliance markdown).
* Bot-generated PRs (Dependabot, Renovate) bypass agentic prose checks but must still pass deterministic SAST/SCA and structural approval routing. Do not blanket-skip them.
Suppression rules exist for developer velocity. Over-broad suppression hides findings; under-broad suppression destroys CI signal-to-noise. Tune iteratively against false-positive rates.
## Toolchain orchestration model
The skill acts as an orchestration layer over established open-source engines. It does not reimplement AST parsers or regex engines. The architecture pattern:
1. Run deterministic engines in parallel (Checkov, Trivy, GitLeaks, OPA/Rego policies, Prowler/AuditKit for cloud API state).
2. Aggregate outputs into a normalized format (SARIF or unified JSON).
3. Use RAG against `.compliance/` markdown to extract policy requirements.
4. Prompt the LLM to map each technical finding to the relevant TSC identifier, cite the violated policy clause, and emit a remediation block.
5. Post results as PR comments and gate the merge if any critical or policy-violating finding is present.
Tool-to-criterion mapping table and prompt patterns: `references/violations-and-tools.md`.
## Honest limits
Document these explicitly when shipping any SOC 2 automation. Misrepresenting them invites false confidence and audit findings.
* **Configuration drift**: the repo represents *intended* state. A console-driven change to AWS bypasses the scanner entirely. Repository scanning needs a runtime CSPM counterpart (e.g., Prowler against the live cloud account) to close the loop.
* **Physical controls (CC6.4, CC6.5)**: badge access, server room locks, MDM enforcement on laptops. The scanner can verify policy artifacts mandate these but cannot verify execution.
* **Human and HR controls (CC1, CC6.2 offboarding)**: SSO enforcement is verifiable; whether a terminated employee's tertiary SaaS access was revoked within the SLA is not, unless the HR system is wired to the identity provider with auditable logs.
* **LLM hallucination over policy prose**: ambiguous or contradictory policy text degrades agentic reliability. Recommend declarative, unambiguous policy authoring (numbered constraints, quantified thresholds) to minimize misinterpretation. For high-stakes findings, require the agent to quote the exact policy clause it relied on.
## Output format for findings
When emitting an audit finding, use this structure so it can be aggregated, deduplicated, and shipped to evidence storage:
```
finding:
criterion: CC6.1
related_criteria: [C1.1, CIS-3, NIST-AC-3, ISO-A.9.4.2]
status: FAIL
type: deterministic # or: agentic, structural
source_tool: checkov
source_finding_id: CKV_AWS_17
file: terraform/rds.tf
line: 42
evidence: "aws_db_instance.primary has storage_encrypted = false"
policy_clause: "Information_Security_Policy.md §4.2: 'All databases storing customer data must use AES-256 encryption at rest.'"
remediation: |
storage_encrypted = true
kms_key_id = aws_kms_key.rds.arn
blocking: true
```
Include the related-criteria array so a single technical check yields multi-framework evidence. This is what makes a SOC 2 scanner economically viable for organizations also pursuing ISO 27001 or FedRAMP.
## When responding to questions about specific criteria
If asked "what does CC6.1 cover" or "is X a SOC 2 violation":
1. Open `references/common-criteria.md` (or `optional-tsc.md`).
2. Locate the criterion.
3. Distinguish deterministic, agentic, and out-of-repo aspects.
4. Cite the AICPA criterion identifier exactly (e.g., `CC6.1`, not "Common Criteria 6.1" or "Section 6.1").
5. If a cross-framework mapping is relevant, pull from `references/cross-framework.md`.
6. If a real-world failure pattern matches the question, cite from `references/violations-and-tools.md`.
Do not paraphrase criterion text from training data. The catalog in references is the canonical source for this skill.
@@ -0,0 +1,146 @@
# Canonical Document Set and RAG Patterns
The agentic auditor reasons over the repository's policy artifacts to extract constraints, then validates technical reality against them. This requires a stable, predictable document layout.
## Expected directory structure
```
.compliance/ # or docs/security/
├── policies/
│ ├── Information_Security_Policy.md
│ ├── Access_Control_Policy.md
│ ├── Change_Management_SDLC.md
│ ├── Vendor_Management_Policy.md
│ ├── Incident_Response_Plan.md
│ ├── Data_Classification_Handling.md
│ ├── Business_Continuity_Plan.md
│ ├── Disaster_Recovery_Plan.md
│ ├── Physical_Security_Policy.md
│ └── Privacy_Policy.md
├── risk/
│ ├── risk_register.csv
│ └── threat_models/
│ └── *.md
├── vendors/
│ ├── vendor_inventory.csv
│ └── reports/
│ └── *.pdf # third-party SOC 2 reports
├── evidence/ # generated; gitignored or LFS
│ ├── type_i/
│ └── type_ii/
└── mappings/
└── tsc_to_controls.yaml # criterion to internal control IDs
```
Repository-root artifacts (CC2.3, CC1.1):
```
SECURITY.md # responsible disclosure
CODE_OF_CONDUCT.md # tone-at-the-top
```
## Document-to-criterion mapping
| Document | Primary criteria | Agentic reasoning intent |
|---|---|---|
| Information_Security_Policy.md | CC1.1, CC2.1, CC5.1, C1.1 | Global constraints (encryption mandates, retention, complexity rules). Source of truth for generating Rego/OPA policies. |
| Access_Control_Policy.md | CC6.1, CC6.2, CC6.3 | RBAC schemas, password rules. Cross-reference against IAM JSON and Kubernetes RoleBindings. |
| Change_Management_SDLC.md | CC8.1, CC4.1, CC4.2 | Approved-change definition. Defines required reviewers, mandatory checks, ticket-linkage requirements. |
| Vendor_Management_Policy.md | CC9.2 | Third-party risk thresholds. Drives SBOM evaluation and vendor SOC 2 freshness checks. |
| Incident_Response_Plan.md | CC7.3, CC7.4, CC7.5 | Containment, eradication, post-mortem procedures. Verify structural completeness. |
| Data_Classification_Handling.md | C1.1, C1.2, P1.1, P4.1 | Tier definitions. Drives encryption-tier matching and retention enforcement. |
| Business_Continuity_Plan.md | CC9.1, A1.2 | RTO, RPO, alternate-site procedures. |
| Disaster_Recovery_Plan.md | A1.2, A1.3 | Restore procedures and test cadence. |
| Physical_Security_Policy.md | CC6.4, CC6.5 | Existence-check only; substance is out-of-repo. |
| Privacy_Policy.md | P1 through P8 | Notice coverage, retention durations, third-party disclosure list. |
| risk_register.csv | CC3.1, CC3.4 | Identified risks and current mitigations. |
| threat_models/*.md | CC3.1, CC3.3 | STRIDE/LINDDUN outputs. Trigger updates after architecture-touching merges. |
| vendor_inventory.csv | CC9.2 | Vendor list with criticality and SOC 2 status. |
| tsc_to_controls.yaml | All | Internal control ID-to-criterion mapping. The ground-truth crosswalk. |
## RAG prompt patterns
The skill uses retrieval-augmented generation: pull the relevant policy section, attach the relevant technical evidence, ask the LLM to evaluate the match.
### Pattern 1: Policy-derived deterministic check
Used when the policy declares a quantified constraint that maps directly to a repo-state field.
```
Context:
Policy clause (Information_Security_Policy.md §4.2):
"All databases storing customer data must use AES-256 encryption at rest with customer-managed KMS keys."
Repo state (terraform/rds.tf, parsed AST):
aws_db_instance.primary:
storage_encrypted: false
kms_key_id: null
Task:
Evaluate against SOC 2 CC6.1 and C1.1.
Output: PASS or FAIL, with the policy clause cited and the exact code remediation block.
```
### Pattern 2: Structural completeness check on policy artifact
Used when the criterion requires a policy section to address specific topics.
```
Context:
Document (Incident_Response_Plan.md): <full markdown body>
SOC 2 CC7.3 to CC7.5 require the IR plan to define:
- event classification scheme
- containment procedure
- eradication procedure
- recovery procedure
- post-mortem procedure
- external communication template
Task:
For each required section, output PRESENT/ABSENT and quote the heading or first sentence if present.
```
### Pattern 3: Cross-document consistency check
Used to detect drift between policy and implementation.
```
Context:
Change_Management_SDLC.md §3:
"Pull requests modifying production infrastructure require approval from at least two members of the Platform Security team."
Branch protection state (GitHub API):
required_approving_review_count: 1
require_code_owner_reviews: true
dismiss_stale_reviews: false
CODEOWNERS for terraform/production/:
@org/platform-security
Task:
Evaluate against SOC 2 CC5.3 (deployment of policies and procedures).
Identify any gap between policy text and technical state.
```
### Pattern 4: Hierarchical reasoning
Used when one criterion depends on a chain of policy and state.
```
Step 1: Parse Change_Management_SDLC.md.
Step 2: Extract the required number of reviewers and required status checks.
Step 3: Query the Git provider API for the branch protection rule on `main`.
Step 4: Compare. Emit PASS/FAIL against CC5.1, CC5.3, CC8.1.
```
## Policy authoring guidance for agentic reliability
The LLM's failure mode over policy prose is misinterpretation, not retrieval failure. Reduce risk by writing policies in a declarative style:
* Quantify thresholds. "Strong passwords" is ambiguous; "minimum 14 characters, plus complexity rules below" is parseable.
* Number constraints. Each requirement gets its own clause ID (`§4.2.1`, `§4.2.2`) so findings can cite a specific rule.
* Avoid contradictions across documents. If two policies disagree, the agent will pick one non-deterministically. Add a precedence statement to the Information Security Policy.
* Use a machine-readable shadow. For high-stakes constraints, accompany the prose with a YAML or Rego encoding under `mappings/`. The agent then has a deterministic source and the prose serves as human-readable documentation.
When you ask the agent to evaluate a constraint, require it to quote the exact clause it relied on. This makes findings auditable and surfaces hallucinations during review.
@@ -0,0 +1,177 @@
# Common Criteria CC1 through CC9
The mandatory Security category. Every SOC 2 report includes these. Each entry lists the criterion text, the deterministic repository check, the agentic reasoning hook (where applicable), and the out-of-repo limit.
Cite criterion identifiers exactly: `CC1.1`, `CC6.1`, etc.
## CC1: Control Environment
Organizational integrity, ethical values, board oversight, structural standards.
### CC1.1 - Tone at the top, integrity, ethical values
* **Deterministic**: presence of `CODE_OF_CONDUCT.md`; presence of an employee handbook acknowledgment registry in the compliance directory.
* **Agentic**: parse the Code of Conduct to ensure it explicitly addresses contractor and vendor conduct (2022 points-of-focus update).
* **Out of repo**: actual ethical conduct in board meetings, day-to-day management behavior.
### CC1.2 - Board independence
* **Deterministic**: existence of a Board Charter document.
* **Out of repo**: financial independence of board members, actual oversight quality.
### CC1.3 to CC1.5 - Management structure, competence, accountability
Includes 2022 update on privacy reporting lines.
* **Deterministic**: organizational chart artifact (`org_chart.yaml`) and role definitions mapped to GitHub teams; CISO reporting line documented.
* **Agentic**: confirm the org chart explicitly defines a privacy and security reporting line.
* **Out of repo**: actual hiring rigor, real disciplinary actions, performance review quality.
## CC2: Communication and Information
The entity generates and uses relevant, quality information internally and externally.
### CC2.1 - Generates relevant information
* **Deterministic**: parse logging IaC (Datadog, Splunk, ELK, OpenSearch configurations) for declared aggregation pipelines and retention.
### CC2.2 - Internal communication
* **Deterministic**: CI/CD webhooks routing security alerts (Dependabot failures, SAST findings) to internal Slack or Teams channels.
### CC2.3 - External communication
* **Deterministic**: `SECURITY.md` in repository root with responsible disclosure and (if applicable) bug bounty instructions.
## CC3: Risk Assessment
Specifies objectives, identifies risks, assesses fraud potential.
### CC3.1 - Suitable objectives
* **Deterministic**: presence and parseability of `risk_register.csv` or markdown threat models (STRIDE, LINDDUN outputs).
### CC3.2 - Identifies and analyzes risk (vulnerabilities)
* **Deterministic**: connect to Dependabot, Snyk, Trivy outputs; assess current dependency CVE severities.
* **Agentic**: compare active CVE severity distribution against the thresholds defined in the Risk Management Policy (e.g., "no unpatched critical for >7 days").
### CC3.3 - Fraud risk
* **Deterministic**: scan IaC for WAF deployment, rate limiting, anti-credential-stuffing controls.
### CC3.4 - Changes affecting internal control
* **Deterministic**: track update frequency of threat-model documentation following major architecture PR merges. Flag if an architecture-touching PR (changes to `terraform/network/`, `helm/`, etc.) merged without a corresponding threat-model update.
## CC4: Monitoring Activities
Ongoing evaluation of internal control effectiveness.
### CC4.1 - Ongoing and separate evaluations
* **Deterministic**: parse `.github/workflows/` or `.gitlab-ci.yml`. Verify SAST, DAST, SCA jobs are explicitly defined, mandatory (`if:` conditions do not allow skip), and execute on every commit or daily cron.
* **Type II requirement**: verify these jobs ran continuously over the observation window with no extended deactivation.
### CC4.2 - Evaluates and communicates deficiencies
* **Deterministic**: pipelines configured to *block* merges on security job failures. A "warn-only" SAST job fails this criterion. Branch protection must require the security check as a status check.
## CC5: Control Activities
Selects and develops control activities that mitigate risks to acceptable levels.
### CC5.1 - Selects control activities
* **Deterministic**: branch protection API state. `required_pull_request_reviews.required_approving_review_count >= 1` (typically `>= 2` for higher-assurance environments). `dismiss_stale_reviews = true`. `require_code_owner_reviews = true` for sensitive paths.
### CC5.2 - Technology general controls
* **Deterministic**: scan Terraform/CloudFormation for systematic baseline rules: no public IPs on databases, default-deny security groups, mandatory tagging.
### CC5.3 - Deploys through policies and procedures
* **Agentic**: map the technical branch-protection state from CC5.1 back to the prose in `Change_Management_SDLC.md`. The mapping is the proof of policy-to-implementation deployment.
## CC6: Logical and Physical Access Controls
Most technically dense category. Governs access restriction and boundary protection.
### CC6.1 - Logical access implementation
* **Deterministic**:
* Git provider API: organization-wide MFA enforced.
* IaC IAM password policy: minimum complexity and rotation.
* Secret scanning (GitLeaks, GitHub Advanced Security) over full commit history. Any AWS key, GitHub PAT, Stripe key, or DB URI is a critical fail.
* IaC: encryption-at-rest flags on all storage resources; KMS key declarations.
* **Agentic**: cross-reference the Information Security Policy's encryption mandate (e.g., "AES-256 with customer-managed keys") against the actual KMS configuration in IaC.
### CC6.2 - User registration and authorization, deprovisioning
* **Deterministic**: SSO/SAML integration configured for the Git provider and any other auditable identity-consuming SaaS (so that offboarding flows through one IdP).
* **Out of repo**: actual offboarding completion across non-Git SaaS unless those systems push audit logs into the IdP or SIEM.
### CC6.3 - Access modification, least privilege, segregation of duties
* **Deterministic**:
* Parse `CODEOWNERS`. Sensitive paths (`.github/workflows/`, `terraform/`, `helm/charts/production/`) require approval from designated security or DevOps teams.
* Scan AWS IAM policies, Kubernetes `Role`/`ClusterRole` manifests for wildcard permissions (`Action: "*"`, `Resource: "*"`, `verbs: ["*"]`). Any wildcard outside well-justified service accounts is a fail.
### CC6.4 and CC6.5 - Physical access and devices
* **Out of repo**: badge access, data center locks, MDM enforcement, privacy screens.
* **Deterministic floor**: `Physical_Security_Policy.md` exists and references current procedures.
### CC6.6 - Boundary protection
* **Deterministic**: scan IaC network definitions. Any AWS Security Group or Network ACL with ingress `0.0.0.0/0` on sensitive ports (22 SSH, 3389 RDP, 5432 Postgres, 3306 MySQL, 6379 Redis, 27017 MongoDB) is a critical immediate-block finding. WAF attached to all public endpoints.
### CC6.7 - Restricts transmission
* **Deterministic**: load balancers, API gateways, CDNs enforce TLS 1.2+ exclusively. Plaintext HTTP listeners on public-facing resources are a fail. Inspect ALB/NLB/Cloudfront listener protocols and ssl_policy fields.
### CC6.8 - Malicious software prevention
* **Deterministic**: container image scanning (Trivy, Grype) active in CI; mandatory pre-deploy scan with CVSS threshold gating.
## CC7: System Operations
Incident detection, performance monitoring, response capability.
### CC7.1 - Vulnerability detection
* **Deterministic**: continuous IaC scanning (Checkov, tfsec, Terrascan) active to detect drift and misconfiguration before `apply`.
### CC7.2 - Anomaly detection
* **Deterministic**:
* IaC: AWS CloudTrail enabled (multi-region, log-file validation), GuardDuty enabled, VPC Flow Logs enabled.
* Git provider audit logs streaming to external storage with at least 1-year retention (GitHub native retention is 90 days; Type II requires the export).
### CC7.3 to CC7.5 - Incident response, evaluation, recovery
* **Agentic**: parse `Incident_Response_Plan.md` for containment, eradication, recovery, and post-mortem procedures. Flag missing sections.
* **Out of repo**: actual incident response execution, tabletop exercise outcomes.
## CC8: Change Management
Controlled process for designing, developing, testing, deploying changes. Core CI/CD territory.
### CC8.1 - Authorized, tested, approved changes
* **Deterministic (Type I)**: branch protection state requires PR, requires N approving reviews, requires status checks, requires linear history (or rebase/squash merge), requires signed commits if mandated.
* **Deterministic (Type II)**: GraphQL query of all merged commits on the default branch over the window. Each must trace to a PR with `APPROVED` review state from an authorized reviewer pre-merge and passing required status checks. Force-pushes to protected branches are a CC8.1 exception. Orphaned commits are exceptions.
* **Agentic**: parse PR description bodies. Confirm developers document the *why* and *how* of changes, with linkage to ticket IDs (Jira, Linear, GitHub Issues) as required by the SDLC policy.
## CC9: Risk Mitigation
Business disruption and vendor risk.
### CC9.1 - Business disruption mitigation
* **Agentic**: parse Business Continuity Plan and Disaster Recovery documentation. Flag missing RTO/RPO declarations or stale test dates.
### CC9.2 - Vendor risk management
* **Deterministic**: SCA produces SBOM (CycloneDX or SPDX format) from dependency manifests.
* **Agentic**: read `Vendor_Management_Policy.md`. Compare against an in-repo directory of vendor SOC 2 reports (PDFs or text). Flag vendors used in critical paths whose reports are stale (>13 months) or absent.
@@ -0,0 +1,93 @@
# Cross-Framework Mappings
A single deterministic technical check can produce evidence for multiple frameworks simultaneously. Tag findings with all relevant identifiers so one scan run yields multi-framework audit packages.
The AICPA publishes official mappings of the 2017 Trust Services Criteria to NIST 800-53, ISO 27001, and others. Some mappings are direct; others are subjective and require interpretation.
## SOC 2 to NIST SP 800-53 Revision 5
NIST 800-53 Rev. 5 is the prescriptive control catalog used in FedRAMP and federal systems. Mappings are typically many-to-many.
| SOC 2 Criterion | NIST 800-53 Rev. 5 | Repository check |
|---|---|---|
| CC4.1 Monitoring | CA-7 Continuous Monitoring | Persistent CI/CD security scan execution on every PR. |
| CC5.2 Control Activities | AC-1 Access Control Policy, CM-1 Configuration Management Policy | Canonical policy markdown present and recently reviewed. |
| CC6.1 Logical Access | AC-2 Account Mgmt, AC-3 Access Enforcement, AC-6 Least Privilege, IA-2 Identification and Authentication | IAM policy parse for wildcards; MFA enforcement; SSO configuration. |
| CC6.6 Boundary Protection | AC-17 Remote Access, SC-7 Boundary Protection | Security group ingress rule audit; WAF attachment to public endpoints. |
| CC6.7 Restricts Transmission | SC-8 Transmission Confidentiality, SC-13 Cryptographic Protection | TLS 1.2+ enforcement on listeners. |
| CC7.1 Vulnerability Detection | RA-5 Vulnerability Scanning, SI-2 Flaw Remediation | Continuous IaC scan; SCA scan on every dependency change. |
| CC7.2 Anomaly Detection | AU-2 Audit Events, AU-6 Audit Review, SI-4 System Monitoring | CloudTrail + GuardDuty + VPC Flow Logs IaC presence; audit log streaming. |
| CC8.1 Change Management | CM-2 Baseline Configuration, CM-3 Configuration Change Control, CM-4 Security Impact Analysis | Branch protection state; PR approval history; mandatory status checks. |
| C1.1 Confidentiality (encryption at rest) | SC-28 Protection of Information at Rest | KMS configuration on storage resources. |
| C1.1 Confidentiality (encryption in transit) | SC-8 | TLS listener configuration. |
## SOC 2 to ISO/IEC 27001 Annex A
ISO 27001 maps to the ISMS-specific technical controls in Annex A. The 2013 and 2022 revisions differ in clause numbering; cite the version in use.
### Annex A 2013 mappings
| SOC 2 Criterion | ISO 27001:2013 Annex A | Repository check |
|---|---|---|
| CC1.1 Tone at the top | A.5.1.1 Information Security Policies | Policy artifact presence and acknowledgment registry. |
| CC5.2, CC5.3 Control Activities | A.9.1.1 Access Control Policy, A.12.1.1 Documented Operating Procedures, A.18.1.1 Independent Review | Policy markdown freshness; CODEOWNERS coverage. |
| CC6.1 Logical Access | A.9.2.1 User Registration, A.9.4.2 Secure Log-on, A.13.2.1 Information Transfer Policies | SSO enforcement; TLS configuration; secret scanning. |
| CC6.3 Least Privilege | A.9.2.3 Privileged Access, A.9.4.1 Information Access Restriction | IAM wildcard scan; CODEOWNERS for sensitive paths. |
| CC6.6 Boundary Protection | A.13.1.1 Network Controls, A.13.1.3 Segregation in Networks | Security group and NACL audit. |
| CC7.1 Vulnerability Detection | A.12.6.1 Management of Technical Vulnerabilities | SCA, IaC scan, container scan on every commit. |
| CC8.1 Change Management | A.12.1.2 Change Management, A.14.2.2 System Change Control | Branch protection; PR approval traceability. |
| C1.1 Confidentiality | A.10.1.1 Cryptographic Controls Policy, A.18.1.4 Privacy and PII | KMS + TLS configuration matched against policy. |
### Annex A 2022 changes
ISO 27001:2022 reorganized Annex A into four themes (Organizational, People, Physical, Technological) with 93 controls. Direct one-to-one ports of 2013 references will fail. Notable additions relevant to repo automation:
* A.5.7 Threat intelligence
* A.5.23 Information security for use of cloud services
* A.8.9 Configuration management
* A.8.16 Monitoring activities
* A.8.28 Secure coding
When asked for ISO 27001 mapping, ask which version the user is certifying against if it is not stated.
## SOC 2 to CIS Controls v8
CIS Controls v8 are prioritized, technical safeguards. They map cleanly to deterministic IaC and CI/CD checks.
| SOC 2 Criterion | CIS Control v8 | Repository check |
|---|---|---|
| CC6.1, C1.1 | Control 3: Data Protection | KMS encryption on storage; secret scanning. |
| CC6.6, CC7.1 | Control 4: Secure Configuration of Enterprise Assets and Software | IaC baseline scanning (Checkov); drift blocked at PR. |
| CC6.2, CC6.3 | Control 5: Account Management; Control 6: Access Control Management | SSO; CODEOWNERS; IAM wildcard ban. |
| CC4.1, CC7.1 | Control 7: Continuous Vulnerability Management | SCA, IaC scan, container scan on every PR. |
| CC2.1, CC7.2 | Control 8: Audit Log Management | CloudTrail + log aggregation IaC. |
| CC3.2, CC9.2 | Control 15: Service Provider Management | SBOM generation; vendor SOC 2 freshness. |
| CC8.1, PI1.3 | Control 16: Application Software Security | SAST + DAST + SCA inextricably integrated; coverage gates. |
| CC7.3-CC7.5 | Control 17: Incident Response Management | IR plan structural check. |
## Other mappings worth knowing
* **PCI DSS v4.0**: relevant for payment processors. PCI Requirement 6 (secure systems and applications) maps to CC8.1 + CC7.1. Requirement 8 (access) maps to CC6.1-CC6.3. Requirement 11 (test security) maps to CC4.1 + CC7.1.
* **HIPAA Security Rule**: 45 CFR §164.308 (administrative safeguards) maps loosely to CC1, CC2, CC3. §164.312 (technical safeguards) maps to CC6.
* **GDPR**: Article 32 (security of processing) maps to CC6.1, CC6.7, C1.1. Articles 15-22 (data subject rights) map to P5, P7.
* **FedRAMP**: built on NIST 800-53; use the SOC 2-to-NIST table above as the bridge.
## Output structure for multi-framework findings
Tag every emitted finding with all applicable framework identifiers:
```yaml
finding:
primary_criterion: CC6.1
framework_tags:
soc2: [CC6.1, C1.1]
nist_800_53_r5: [AC-3, IA-2, SC-28]
iso_27001_2013: [A.9.4.2, A.10.1.1]
iso_27001_2022: [A.8.5, A.8.24]
cis_v8: [3.11, 6.7]
pci_dss_v4: [3.5, 8.3]
status: FAIL
evidence: ...
```
This is the multidimensional tagging model. A single scan produces audit-ready evidence packages for every framework the organization pursues.
@@ -0,0 +1,112 @@
# Optional Trust Services Categories
These four categories are included only when applicable to the organization's services, SLAs, or regulatory obligations. The Common Criteria (CC1-CC9) remain mandatory regardless.
## Availability (A1.1 to A1.3)
Systems are accessible for operation and use to meet the entity's commitments and SLAs.
### A1.1 - Capacity and demand
* **Deterministic**: IaC declares auto-scaling groups, Kubernetes HPA/VPA, RDS read replicas, capacity quotas. Verify scaling policies have non-trivial thresholds (not `min=max`).
### A1.2 - Environmental protections, software, data backup, recovery
* **Deterministic**:
* Multi-AZ deployment for production databases (`multi_az = true` for RDS, replica configurations for Postgres, geo-redundant storage for cloud blob).
* Automated snapshot or backup schedules with declared retention.
* Restore procedures referenced in IaC or runbook artifacts.
### A1.3 - Recovery testing
* **Agentic**: parse the DR runbook for declared restore-test cadence. The repo can verify the cadence is *declared*; actual execution evidence is out-of-repo unless restoration runs are scripted in CI (e.g., a scheduled job that restores a snapshot to a sandbox account).
## Confidentiality (C1.1 to C1.2)
Information designated as confidential is protected throughout its lifecycle.
### C1.1 - Identifies and maintains confidential information
* **Deterministic**:
* Encryption-at-rest: KMS-backed encryption on all storage (`storage_encrypted = true`, `kms_key_id` set, EBS volumes encrypted, S3 SSE-KMS).
* Encryption-in-transit: TLS 1.2+ on all listeners, no plaintext HTTP on public endpoints.
* Secret scanning across commit history (overlaps with CC6.1).
* **Agentic**: parse `Data_Classification_Handling.md`. For each data tier (Public, Internal, Confidential, Restricted), confirm IaC controls match. Restricted-tier resources should use customer-managed KMS keys and dedicated tenancy where the policy mandates it.
### C1.2 - Disposes of confidential information
* **Deterministic**: S3 lifecycle rules, RDS final snapshot policies, log retention bounds. Verify destruction is configured, not just paused.
## Processing Integrity (PI1.1 to PI1.5)
System processing is complete, valid, accurate, timely, and authorized.
### PI1.1 - Quality of processing inputs
* **Deterministic**: presence of input validation libraries or schemas (e.g., Zod, Pydantic, JSON Schema validators) in critical service entry points.
### PI1.2 - System inputs are complete, accurate
* **Agentic**: read processing-flow documentation. Confirm declared validation gates exist as code.
### PI1.3 - System processing produces complete and accurate outputs
* **Deterministic**: query CI/CD coverage reports (JaCoCo, Codecov, Istanbul). Coverage thresholds must meet or exceed the SDLC policy's stated minimum (often 70-80% for new code). Verify the threshold is enforced as a blocking status check, not advisory.
### PI1.4 - Output is delivered to authorized parties
* **Agentic**: parse webhook configurations and outbound integrations. Confirm authentication is required on all outputs.
### PI1.5 - System processing is authorized
* **Deterministic**: API authentication and authorization middleware is present on all routes (no anonymous mutating endpoints unless explicitly justified).
## Privacy (P1.1 to P8.1)
Collection, use, retention, disclosure, and disposal of personal information per the entity's privacy notice.
The privacy category has the most criteria (P1 through P8). Treat each subgroup as a checklist.
### P1 - Notice and communication of objectives
* **Agentic**: parse the public privacy notice (often `PRIVACY.md` or a CMS-managed page mirrored in the repo). Confirm it covers the categories of information collected, uses, third-party sharing, retention, and rights.
### P2 - Choice and consent
* **Deterministic**: consent-tracking logic in code. Cookie banners, opt-in flags, and consent log persistence are present.
### P3 - Collection
* **Agentic**: confirm data collection points in code (forms, API endpoints) match the notice. Flag fields collected in code but not declared in the notice.
### P4 - Use, retention, disposal
* **Deterministic**: data retention lifecycle configured. S3 object expiration rules, database retention jobs, log expiration. Retention durations match the privacy notice.
* **Agentic**: cross-reference declared retention in the privacy notice against IaC lifecycle policies.
### P5 - Access
* **Deterministic**: subject access request (SAR) endpoints or admin tooling exist. For regulated regions (GDPR Art. 15, CCPA), a documented mechanism is mandatory.
### P6 - Disclosure to third parties
* **Agentic**: read vendor list and DPAs. Confirm each external data flow has a documented legal basis.
### P7 - Quality
* **Deterministic**: data correction endpoints (GDPR Art. 16) exist.
### P8 - Monitoring and enforcement
* **Agentic**: parse the privacy policy for declared review cadence and enforcement procedures. Verify a designated privacy owner is named (DPO if required).
## Scope decision
When asked which optional categories apply:
* **Availability** is required if the SLA promises uptime to customers.
* **Confidentiality** is required if customer-classified-confidential data is processed.
* **Processing Integrity** is required for transaction processing, financial calculation, payments, or anything where output correctness is contractually or legally significant.
* **Privacy** is required if PII is collected and the entity has obligations under GDPR, CCPA, HIPAA-adjacent rules, or its own privacy notice.
Most B2B SaaS scopes Security + Availability + Confidentiality. Payment processors add Processing Integrity. Consumer-facing or PII-heavy services add Privacy.
@@ -0,0 +1,220 @@
# Violation Patterns and Toolchain Orchestration
Organizations rarely fail SOC 2 audits because they lack policies. They fail because operational reality diverges from documentation. This file catalogs the empirical violation patterns the scanner must hunt and the open-source engines used to detect them.
## Real-world violation patterns
### 1. IaC misconfiguration (CC6.1, CC6.6, C1.1)
IaC dictates the actual security posture of deployed infrastructure. Insecure code yields an insecure environment.
**Common failures:**
* `aws_db_instance` or `aws_rds_cluster` with `storage_encrypted = false` or missing `kms_key_id`.
* `aws_s3_bucket` with `block_public_acls = false`, missing server-side encryption, or no bucket policy denying unencrypted PUTs.
* `aws_security_group` with ingress rule `cidr_blocks = ["0.0.0.0/0"]` on ports 22, 3389, 5432, 3306, 6379, 27017, or any database port.
* `aws_ebs_volume` without `encrypted = true`.
* Kubernetes manifests with `hostNetwork: true`, `privileged: true`, or `runAsUser: 0` outside justified system pods.
**Detection:** AST parse of HCL/YAML/JSON. Use Checkov, tfsec, or Terrascan with rule packs aligned to SOC 2 + CIS. Block PR on critical findings.
### 2. IAM over-privilege (CC6.3)
Developers grant wildcard permissions to bypass deployment friction. This is the single most common SOC 2 finding in cloud-native shops.
**Common failures:**
* AWS IAM policy: `Action: "*"`, `Resource: "*"`, `Effect: Allow`.
* Kubernetes `ClusterRole` with `verbs: ["*"]` on `resources: ["secrets"]`.
* GCP IAM bindings with `roles/owner` on a service account.
* Azure: Owner role assigned to non-human principals.
**Detection:** static analysis of IAM JSON, Helm charts, Kubernetes manifests. Flag wildcards. Require scoping to exact ARNs or resource names before merge. Allow exceptions only via documented justification in the PR description that the agentic layer reviews.
### 3. Cryptographic secret sprawl (CC6.1, C1.1)
The leading vector for systemic breaches.
**Common failures:**
* Hardcoded `AKIA...` (AWS access key), `ghp_...` (GitHub PAT), `sk_live_...` (Stripe), `xoxb-...` (Slack bot token).
* Committed `.env` files containing DB URIs, API tokens.
* Private keys (`-----BEGIN RSA PRIVATE KEY-----`) in source.
* `password = "..."` literals in IaC.
**Detection:** GitLeaks, GitHub Advanced Security secret scanning, TruffleHog. High-precision regex plus entropy detection across the *entire commit history* (not just the current diff; old commits remain in the public history of forks).
**Remediation requirement:** finding a leaked secret means rotation, not just removal. The scanner should generate a remediation playbook citing the rotation procedure for the specific provider.
### 4. Change management bypass (CC8.1)
Auditors search ruthlessly for breaks in the chain of custody.
**Common failures:**
* Repository owner temporarily disables branch protection to force-push a hotfix, then re-enables it.
* Direct push to `main` by an admin bypassing PR requirement.
* Self-approved PR (developer is the only reviewer in `CODEOWNERS` for the touched path).
* PR merged before required status checks completed (status check defined as required but not enforcing).
* Force push to `main` rewriting history.
**Detection:**
* Type I miss: a point-in-time check sees branch protection enabled and passes.
* Type II catch: GraphQL query of all commits on `main` over the window. For each commit, fetch the PR. Verify pre-merge `APPROVED` review by a non-author authorized reviewer and passing required status checks. Orphaned commits, force-pushes, and self-merges are exceptions.
GitHub provides this audit trail via the audit log API (org-level) or the commit and PR REST/GraphQL APIs. Stream the audit log to immutable storage; native retention is 90 days.
### 5. Dependency vulnerability accumulation (CC3.2, CC9.2)
Supply chain risk is heavily scrutinized.
**Common failures:**
* `package-lock.json` containing transitive dependencies with known critical CVEs (Log4Shell-class) unpatched for >30 days.
* `go.sum` with vulnerable indirect dependencies.
* Container base images on EOL distros (Alpine 3.10, Debian 9).
* Pinned versions of libraries with public exploits available, no upgrade PR open.
**Detection:** Trivy, Snyk, Dependabot, OSV-Scanner. Track *time-to-remediation* against the SLA in the Risk Management Policy. Aging reports drive Type II evidence.
### 6. Pipeline self-tampering (CC4.1, CC4.2)
A subtle but high-impact failure: a developer modifies the CI workflow to skip security jobs.
**Common failures:**
* Adding `if: github.event.pull_request.user.login != 'bot-account'` to a security job (skipping it for some PRs).
* Changing `continue-on-error: false` to `true` on a SAST step, downgrading it from blocking to advisory.
* Deleting or commenting out a Trivy or Checkov step.
**Detection:** scan diffs to `.github/workflows/`, `.gitlab-ci.yml`, `Jenkinsfile`. Any change reducing security-job strictness must be flagged as a high-severity CC4 finding requiring security-team approval. Treat this category as the most important because it can hide every other finding.
### 7. Logging and audit gap (CC7.2)
**Common failures:**
* `aws_cloudtrail` resource missing or with `is_multi_region_trail = false`, `enable_log_file_validation = false`.
* No `aws_guardduty_detector` resource.
* VPC Flow Logs not configured.
* Application logs lack PII redaction, leaking confidential data into long-term storage.
**Detection:** IaC parse for required logging resources. Application-level: scan logging configurations for redaction filters on fields tagged Confidential or Restricted in `Data_Classification_Handling.md`.
## Open-source toolchain orchestration
The scanner is an orchestration layer over best-in-class engines. It does not reimplement parsers.
### Engine inventory
| Tool | Domain | Notes |
|---|---|---|
| **Checkov** | IaC scanning (Terraform, CloudFormation, Kubernetes, Helm, ARM, Serverless) | 1000+ built-in policies aligned to SOC 2, CIS, NIST, HIPAA. Output: SARIF, JSON, CSV. Custom Python policies supported. |
| **tfsec / Trivy IaC** | IaC scanning | Faster than Checkov on large monorepos; narrower built-in policy set. |
| **Trivy** | Container image scanning, SCA, IaC, secret scanning | Single binary covers four domains. Strong CVE database. |
| **Snyk** | SCA, container scan, IaC, code scanning | Commercial; free tier for OSS. Better remediation suggestions than OSS alternatives. |
| **GitLeaks** | Secret scanning across Git history | Fast Go binary. Custom rule support via TOML. |
| **TruffleHog** | Secret scanning with verification | Validates leaked credentials by attempting to authenticate. Higher precision than entropy-only tools. |
| **OPA / Gatekeeper** | Policy as code (Rego) | Use for organization-specific constraints (e.g., "only US-East regions allowed"). Reads naturally as the executable form of `Information_Security_Policy.md`. |
| **Conftest** | Apply OPA policies to config files | Wrapper for testing Kubernetes/Terraform/Dockerfile/Helm against Rego policies. |
| **Semgrep** | SAST (custom rule patterns) | Multi-language; rule packs for common vulnerabilities. |
| **CodeQL** | SAST (deep semantic analysis) | GitHub-native; broader detection than Semgrep but slower and heavier. |
| **Prowler / AuditKit** | Cloud API state checks | Runtime CSPM; not pure repo-scoped but useful for org-level GitHub API checks (MFA enforcement, audit log streaming). |
| **OSV-Scanner** | SCA against OSV database | Lightweight; covers many ecosystems. Good supplement to Trivy. |
### Orchestration architecture
```
PR webhook
│
▼
┌─────────────────────────────────────────────────┐
│ Trigger evaluation │
│ - Files changed? │
│ - Draft PR? │
│ - Bot author? │
│ - Modifies .github/workflows/? │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Parallel deterministic scans │
│ ├── Checkov (IaC) → SARIF │
│ ├── Trivy (containers) → JSON │
│ ├── Trivy (SCA) → JSON │
│ ├── GitLeaks (secrets) → JSON │
│ ├── Conftest (custom OPA) → JSON │
│ ├── Semgrep (SAST) → SARIF │
│ └── GitHub API queries → JSON │
│ (branch protection, audit log config) │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Normalize to unified schema │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ RAG over .compliance/ + agentic mapping │
│ - Pull relevant policy clause │
│ - Map technical finding to TSC + framework tags│
│ - Generate remediation block │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ PR comment (grouped by criterion) │
│ + Status check (blocking on critical) │
│ + Evidence persisted to .compliance/evidence/ │
└─────────────────────────────────────────────────┘
```
### Prompt template for the agentic mapping layer
```
System:
You are a SOC 2 compliance auditor. Given a parsed security finding from a
deterministic engine and the relevant policy clause, produce an audit
finding mapped to the appropriate Trust Services Criteria.
Output schema (YAML):
primary_criterion: <CC or optional-category identifier>
related_criteria: [list]
framework_tags: {soc2: [...], nist_800_53_r5: [...], iso_27001_2013: [...], cis_v8: [...]}
status: PASS | FAIL
severity: critical | high | medium | low
type: deterministic | agentic
source_tool: <tool name>
file: <path>
line: <number or null>
evidence: <one-sentence factual description>
policy_clause: <quoted text from .compliance/ with file path and section>
remediation: |
<exact code block to fix>
blocking: <bool>
User:
Finding from Checkov:
rule_id: CKV_AWS_17
file: terraform/rds.tf
line: 42
description: Ensure RDS instances have storage encrypted
Policy clause from Information_Security_Policy.md §4.2:
"All databases storing customer data must use AES-256 encryption at rest
with customer-managed KMS keys."
Produce the audit finding.
```
### Cost and latency considerations
* Run deterministic engines in parallel; total wall-clock is bounded by the slowest (typically Checkov on a large IaC tree, 30-90 seconds).
* Cache scan results by content hash. Re-running the scanner on a no-op rebase should be near-free.
* Agentic mapping is the expensive step in dollar terms. Batch findings: send 10-20 findings per LLM call, not one at a time.
* For Type II evidence collection (history-walk), schedule as nightly batch, not per-PR. The PR-time scanner produces Type I evidence; the nightly walker accumulates Type II.
### Failure mode: tool drift
Open-source engines update rule packs frequently. A passing scan today may fail tomorrow because Checkov shipped a new rule. Pin the engine version in CI. Promote upgrades through a PR like any other dependency. Treat rule-pack diffs as risk-assessment input (CC3.4: changes affecting internal control).
+8 -513
View File
@@ -1,515 +1,10 @@
'use client'
import { redirect } from 'next/navigation'
import { useState, useEffect } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { useToast } from '@/components/ui/use-toast'
import { getErrorMessage } from '@/lib/errors/get-error-message'
import { ArrowLeft, CheckCircle, CreditCard, FileText, Trash2 } from 'lucide-react'
import Link from 'next/link'
import { AccountNumber } from '@/components/ui/account-number'
import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog'
import type { SupplierInvoice, SupplierInvoiceItem, SupplierInvoicePayment, EntityType } from '@/types'
const statusConfig: Record<string, { label: string; color: string }> = {
registered: { label: 'Obetald', color: 'bg-blue-100 text-blue-800' },
approved: { label: 'Obetald', color: 'bg-yellow-100 text-yellow-800' },
paid: { label: 'Betald', color: 'bg-success/10 text-success' },
partially_paid: { label: 'Delbetald', color: 'bg-orange-100 text-orange-800' },
overdue: { label: 'Förfallen', color: 'bg-destructive/10 text-destructive' },
disputed: { label: 'Tvist', color: 'bg-purple-100 text-purple-800' },
credited: { label: 'Krediterad', color: 'bg-gray-100 text-gray-800' },
}
function formatAmount(amount: number): string {
return amount.toLocaleString('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
export default function ExpenseDetailPage() {
const params = useParams()
const router = useRouter()
const { toast } = useToast()
const [invoice, setInvoice] = useState<SupplierInvoice | null>(null)
const [, setEntityType] = useState<EntityType>('enskild_firma')
const [isLoading, setIsLoading] = useState(true)
const [isPayDialogOpen, setIsPayDialogOpen] = useState(false)
const [payAmount, setPayAmount] = useState('')
const [paymentDate, setPaymentDate] = useState(() => new Date().toISOString().split('T')[0])
const [isProcessing, setIsProcessing] = useState(false)
const { dialogProps: confirmDialogProps, confirm: confirmAction } = useDestructiveConfirm()
async function fetchInvoice() {
setIsLoading(true)
const res = await fetch(`/api/supplier-invoices/${params.id}`)
const { data, error } = await res.json()
if (error) {
toast({ title: 'Kunde inte ladda utgift', description: error, variant: 'destructive' })
} else {
setInvoice(data)
setPayAmount(String(data.remaining_amount))
setPaymentDate(new Date().toISOString().split('T')[0])
}
setIsLoading(false)
}
async function fetchEntityType() {
try {
const res = await fetch('/api/settings')
const { data } = await res.json()
if (data?.entity_type) {
setEntityType(data.entity_type)
}
} catch {
// Default to enskild_firma
}
}
useEffect(() => {
fetchInvoice()
fetchEntityType()
}, [params.id])
async function handleApprove() {
setIsProcessing(true)
const res = await fetch(`/api/supplier-invoices/${params.id}/approve`, { method: 'POST' })
const result = await res.json()
if (!res.ok) {
toast({ title: 'Kunde inte godkänna', description: getErrorMessage(result, { context: 'supplier_invoice' }), variant: 'destructive' })
} else {
toast({ title: 'Godkänd', description: 'Utgiften har godkänts' })
fetchInvoice()
}
setIsProcessing(false)
}
async function handleMarkPaid() {
setIsProcessing(true)
const res = await fetch(`/api/supplier-invoices/${params.id}/mark-paid`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: parseFloat(payAmount), payment_date: paymentDate }),
})
const result = await res.json()
if (!res.ok) {
toast({ title: 'Betalning misslyckades', description: getErrorMessage(result, { context: 'supplier_invoice' }), variant: 'destructive' })
} else {
toast({
title: result.status === 'paid' ? 'Betald' : 'Delbetalning registrerad',
description: `${formatAmount(parseFloat(payAmount))} kr registrerat`,
})
setIsPayDialogOpen(false)
fetchInvoice()
}
setIsProcessing(false)
}
async function handleCredit() {
const ok = await confirmAction({
title: 'Registrera kreditfaktura',
description: 'En kreditfaktura skapas som reverserar den ursprungliga fakturan. Denna åtgärd kan inte ångras.',
confirmLabel: 'Registrera kreditfaktura',
variant: 'warning',
})
if (!ok) return
setIsProcessing(true)
const res = await fetch(`/api/supplier-invoices/${params.id}/credit`, { method: 'POST' })
const result = await res.json()
if (!res.ok) {
toast({ title: 'Kunde inte kreditera', description: getErrorMessage(result, { context: 'supplier_invoice' }), variant: 'destructive' })
} else {
toast({ title: 'Kreditfaktura registrerad' })
fetchInvoice()
}
setIsProcessing(false)
}
async function handleDelete() {
const ok = await confirmAction({
title: 'Ta bort utgift',
description: 'Utgiften och tillhörande data tas bort permanent. Denna åtgärd kan inte ångras.',
confirmLabel: 'Ta bort',
variant: 'destructive',
})
if (!ok) return
const res = await fetch(`/api/supplier-invoices/${params.id}`, { method: 'DELETE' })
const result = await res.json()
if (!res.ok) {
toast({ title: 'Kunde inte ta bort', description: getErrorMessage(result, { context: 'supplier_invoice' }), variant: 'destructive' })
} else {
toast({ title: 'Borttagen' })
router.push('/expenses')
}
}
if (isLoading) {
return (
<div className="space-y-6 max-w-4xl">
<div className="h-8 bg-muted rounded w-48 animate-pulse" />
<Card className="animate-pulse"><CardContent className="h-48" /></Card>
</div>
)
}
if (!invoice) {
return (
<div className="text-center py-12">
<p className="text-muted-foreground">Utgiften hittades inte</p>
<Button variant="outline" className="mt-4" onClick={() => router.push('/expenses')}>
Tillbaka
</Button>
</div>
)
}
const items = (invoice.items || []) as SupplierInvoiceItem[]
const payments = (invoice.payments || []) as SupplierInvoicePayment[]
const status = statusConfig[invoice.status] || { label: invoice.status, color: '' }
return (
<div className="space-y-6 max-w-4xl">
{/* Header */}
<div className="space-y-4">
<div className="flex items-start gap-4">
<Button variant="ghost" size="icon" className="shrink-0 mt-1" onClick={() => router.push('/expenses')}>
<ArrowLeft className="h-5 w-5" />
</Button>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">
Utgift #{invoice.arrival_number}
</h1>
<Badge className={status.color}>
{status.label}
</Badge>
</div>
<p className="text-muted-foreground truncate">
{invoice.supplier?.name} · Faktura {invoice.supplier_invoice_number}
</p>
</div>
</div>
{/* Context-aware actions */}
<div className="flex flex-col sm:flex-row gap-2 sm:justify-end">
{invoice.status === 'registered' && (
<>
<Button className="w-full sm:w-auto" onClick={handleApprove} disabled={isProcessing}>
<CheckCircle className="mr-2 h-4 w-4" />
Godkänn
</Button>
<Button variant="destructive" className="w-full sm:w-auto" onClick={handleDelete} disabled={isProcessing}>
<Trash2 className="mr-2 h-4 w-4 sm:mr-0" />
<span className="sm:hidden">Ta bort</span>
</Button>
</>
)}
{['approved', 'overdue'].includes(invoice.status) && (
<>
<Button className="w-full sm:w-auto" onClick={() => setIsPayDialogOpen(true)} disabled={isProcessing}>
<CreditCard className="mr-2 h-4 w-4" />
Markera betald
</Button>
<Button variant="outline" className="w-full sm:w-auto" onClick={handleCredit} disabled={isProcessing}>
<FileText className="mr-2 h-4 w-4" />
Kreditfaktura
</Button>
</>
)}
{invoice.status === 'partially_paid' && (
<Button className="w-full sm:w-auto" onClick={() => setIsPayDialogOpen(true)} disabled={isProcessing}>
<CreditCard className="mr-2 h-4 w-4" />
Registrera betalning
</Button>
)}
</div>
</div>
{/* Card 1: Fakturadetaljer */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Fakturadetaljer</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Info grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Leverantör</span>
<p className="font-medium">
{invoice.supplier ? (
<Link href={`/suppliers/${invoice.supplier.id}`} className="text-primary hover:underline">
{invoice.supplier.name}
</Link>
) : '-'}
</p>
{invoice.supplier?.org_number && (
<p className="text-xs text-muted-foreground">Org.nr: {invoice.supplier.org_number}</p>
)}
</div>
<div>
<span className="text-muted-foreground">Fakturanummer</span>
<p className="font-medium">{invoice.supplier_invoice_number}</p>
</div>
<div>
<span className="text-muted-foreground">Ankomstnummer</span>
<p className="font-medium font-mono">{invoice.arrival_number}</p>
</div>
<div>
<span className="text-muted-foreground">Fakturadatum</span>
<p className="font-medium">{invoice.invoice_date}</p>
</div>
<div>
<span className="text-muted-foreground">Förfallodatum</span>
<p className="font-medium">{invoice.due_date}</p>
</div>
{invoice.delivery_date && (
<div>
<span className="text-muted-foreground">Leveransdatum</span>
<p className="font-medium">{invoice.delivery_date}</p>
</div>
)}
{invoice.payment_reference && (
<div>
<span className="text-muted-foreground">OCR/referens</span>
<p className="font-medium font-mono">{invoice.payment_reference}</p>
</div>
)}
</div>
{invoice.reverse_charge && (
<Badge className="bg-purple-100 text-purple-800">Omvänd skattskyldighet</Badge>
)}
{/* Line items */}
{items.length > 0 && (
<div className="border-t pt-4">
{/* Desktop: table */}
<div className="hidden sm:block">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-2">Beskrivning</th>
<th className="pb-2 w-20">Konto</th>
<th className="pb-2 w-16 text-right">Moms%</th>
<th className="pb-2 w-28 text-right">Belopp</th>
<th className="pb-2 w-24 text-right">Moms</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.id} className="border-b last:border-0">
<td className="py-2">{item.description}</td>
<td className="py-2"><AccountNumber number={item.account_number} /></td>
<td className="py-2 text-right">{Math.round(item.vat_rate * 100)}%</td>
<td className="py-2 text-right font-mono">{formatAmount(item.line_total)}</td>
<td className="py-2 text-right font-mono">{formatAmount(item.vat_amount)}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile: stacked cards */}
<div className="sm:hidden space-y-3">
{items.map((item) => (
<div key={item.id} className="border rounded-lg p-3 space-y-2 text-sm">
<div className="flex justify-between items-start gap-2">
<span className="font-medium">{item.description || 'Ingen beskrivning'}</span>
<AccountNumber number={item.account_number} />
</div>
<div className="flex justify-between text-muted-foreground">
<span>Belopp</span>
<span className="font-mono">{formatAmount(item.line_total)}</span>
</div>
<div className="flex justify-between text-muted-foreground">
<span>Moms ({Math.round(item.vat_rate * 100)}%)</span>
<span className="font-mono">{formatAmount(item.vat_amount)}</span>
</div>
</div>
))}
</div>
{/* Amounts summary */}
<div className="mt-4 pt-4 border-t space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Netto (exkl. moms)</span>
<span className="font-mono">{formatAmount(invoice.subtotal)} {invoice.currency}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Moms</span>
<span className="font-mono">{formatAmount(invoice.vat_amount)} {invoice.currency}</span>
</div>
<div className="flex justify-between font-bold text-base pt-2 border-t">
<span>Totalt</span>
<span className="font-mono">{formatAmount(invoice.total)} {invoice.currency}</span>
</div>
<div className="flex justify-between pt-2">
<span className="text-muted-foreground">Betalt</span>
<span className="font-mono text-success">{formatAmount(invoice.paid_amount)} {invoice.currency}</span>
</div>
<div className="flex justify-between font-semibold">
<span>Kvar att betala</span>
<span className="font-mono">{formatAmount(invoice.remaining_amount)} {invoice.currency}</span>
</div>
</div>
</div>
)}
{/* Notes inline */}
{invoice.notes && (
<div className="border-t pt-4">
<p className="text-sm text-muted-foreground">{invoice.notes}</p>
</div>
)}
</CardContent>
</Card>
{/* Card 2: Betalningar & bokföring (only if data exists) */}
{(payments.length > 0 || invoice.registration_journal_entry_id) && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Betalningar & bokföring</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Payment history */}
{payments.length > 0 && (
<div>
<p className="text-sm font-medium mb-2">Betalningshistorik</p>
{/* Desktop: table */}
<div className="hidden sm:block">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-2">Datum</th>
<th className="pb-2 text-right">Belopp</th>
<th className="pb-2">Verifikation</th>
<th className="pb-2">Anteckning</th>
</tr>
</thead>
<tbody>
{payments.map((p) => (
<tr key={p.id} className="border-b last:border-0">
<td className="py-2">{p.payment_date}</td>
<td className="py-2 text-right font-mono">{formatAmount(p.amount)} {p.currency}</td>
<td className="py-2">
{p.journal_entry_id ? (
<Link href={`/bookkeeping/${p.journal_entry_id}`} className="text-primary hover:underline font-mono text-xs">
{p.journal_entry_id.substring(0, 8)}...
</Link>
) : '-'}
</td>
<td className="py-2 text-muted-foreground">{p.notes || '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile: stacked cards */}
<div className="sm:hidden space-y-3">
{payments.map((p) => (
<div key={p.id} className="border rounded-lg p-3 space-y-1 text-sm">
<div className="flex justify-between items-center">
<span className="text-muted-foreground">{p.payment_date}</span>
<span className="font-mono font-medium">{formatAmount(p.amount)} {p.currency}</span>
</div>
{p.journal_entry_id && (
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Verifikation</span>
<Link href={`/bookkeeping/${p.journal_entry_id}`} className="text-primary hover:underline font-mono text-xs">
{p.journal_entry_id.substring(0, 8)}...
</Link>
</div>
)}
{p.notes && (
<p className="text-muted-foreground text-xs pt-1">{p.notes}</p>
)}
</div>
))}
</div>
</div>
)}
{/* Journal entry links */}
<div className="text-sm space-y-2">
<p className="font-medium">Verifikationer</p>
{invoice.registration_journal_entry_id ? (
<div className="flex justify-between">
<span className="text-muted-foreground">Registreringsverifikation</span>
<Link
href={`/bookkeeping/${invoice.registration_journal_entry_id}`}
className="text-primary hover:underline font-mono"
>
{invoice.registration_journal_entry_id.substring(0, 8)}...
</Link>
</div>
) : (
<p className="text-muted-foreground">Ingen registreringsverifikation</p>
)}
{invoice.payment_journal_entry_id && (
<div className="flex justify-between">
<span className="text-muted-foreground">Betalningsverifikation</span>
<Link
href={`/bookkeeping/${invoice.payment_journal_entry_id}`}
className="text-primary hover:underline font-mono"
>
{invoice.payment_journal_entry_id.substring(0, 8)}...
</Link>
</div>
)}
</div>
</CardContent>
</Card>
)}
<DestructiveConfirmDialog {...confirmDialogProps} />
{/* Pay Dialog */}
<Dialog open={isPayDialogOpen} onOpenChange={setIsPayDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Markera som betald</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="payment-date">Betalningsdatum</Label>
<Input
id="payment-date"
type="date"
value={paymentDate}
max={new Date().toISOString().split('T')[0]}
onChange={(e) => setPaymentDate(e.target.value)}
className="w-full sm:w-48"
/>
</div>
<div className="space-y-2">
<Label htmlFor="payment-amount">Belopp att betala</Label>
<Input
id="payment-amount"
type="number"
step="0.01"
value={payAmount}
onChange={(e) => setPayAmount(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
Kvar att betala: {formatAmount(invoice.remaining_amount)} {invoice.currency}
</p>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsPayDialogOpen(false)}>
Avbryt
</Button>
<Button onClick={handleMarkPaid} disabled={isProcessing}>
{isProcessing ? 'Bearbetar...' : 'Registrera betalning'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
)
export default async function ExpenseDetailRedirectPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
redirect(`/supplier-invoices/${id}`)
}
+16 -878
View File
@@ -1,884 +1,22 @@
'use client'
import { redirect } from 'next/navigation'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useForm, Controller, useFieldArray } from 'react-hook-form'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Checkbox } from '@/components/ui/checkbox'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { useToast } from '@/components/ui/use-toast'
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog'
import { SupplierInvoiceReviewContent } from '@/components/suppliers/SupplierInvoiceReviewContent'
import AccountCombobox from '@/components/bookkeeping/AccountCombobox'
import { getAccountDescription } from '@/lib/bookkeeping/account-descriptions'
import { getErrorMessage } from '@/lib/errors/get-error-message'
import { useUnsavedChanges } from '@/lib/hooks/use-unsaved-changes'
import { ArrowLeft, Plus, Trash2, ChevronDown, Loader2, Lock } from 'lucide-react'
import { useCanWrite } from '@/lib/hooks/use-can-write'
import type { Supplier, BASAccount, VatTreatment, EntityType } from '@/types'
type SearchParams = Record<string, string | string[] | undefined>
interface LineItem {
description: string
amount: number
account_number: string
vat_rate: number
}
interface FormData {
supplier_id: string
supplier_invoice_number: string
invoice_date: string
due_date: string
delivery_date: string
currency: string
exchange_rate: string
reverse_charge: boolean
payment_reference: string
notes: string
items: LineItem[]
}
interface NewSupplierForm {
name: string
supplier_type: string
org_number: string
bankgiro: string
plusgiro: string
default_expense_account: string
}
function formatAmount(amount: number): string {
return amount.toLocaleString('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
function inferVatTreatment(items: LineItem[], reverseCharge: boolean): VatTreatment {
if (reverseCharge) return 'reverse_charge'
const rates = new Set(items.map((i) => i.vat_rate))
if (rates.size === 1) {
const rate = rates.values().next().value!
if (rate === 0.25) return 'standard_25'
if (rate === 0.12) return 'reduced_12'
if (rate === 0.06) return 'reduced_6'
if (rate === 0) return 'exempt'
}
return 'standard_25'
}
export default function NewExpensePage() {
const router = useRouter()
const { canWrite } = useCanWrite()
const { toast } = useToast()
const [suppliers, setSuppliers] = useState<Supplier[]>([])
const [accounts, setAccounts] = useState<BASAccount[]>([])
const [entityType, setEntityType] = useState<EntityType>('enskild_firma')
const [isSubmitting, setIsSubmitting] = useState(false)
const [showReview, setShowReview] = useState(false)
const [pendingData, setPendingData] = useState<FormData | null>(null)
const [showNewSupplier, setShowNewSupplier] = useState(false)
const [isCreatingSupplier, setIsCreatingSupplier] = useState(false)
const [pendingSupplierSelect, setPendingSupplierSelect] = useState<string | null>(null)
const [advancedOpen, setAdvancedOpen] = useState(false)
const [newSupplier, setNewSupplier] = useState<NewSupplierForm>({
name: '',
supplier_type: 'swedish_business',
org_number: '',
bankgiro: '',
plusgiro: '',
default_expense_account: '',
})
const { register, control, handleSubmit, watch, setValue, formState: { isDirty } } = useForm<FormData>({
defaultValues: {
supplier_id: '',
supplier_invoice_number: '',
invoice_date: new Date().toISOString().split('T')[0],
due_date: '',
delivery_date: '',
currency: 'SEK',
exchange_rate: '',
reverse_charge: false,
payment_reference: '',
notes: '',
items: [{ description: '', amount: 0, account_number: '5010', vat_rate: 0.25 }],
},
})
useUnsavedChanges(isDirty)
const { fields, append, remove } = useFieldArray({ control, name: 'items' })
const watchedItems = watch('items')
const watchedSupplierId = watch('supplier_id')
const watchedCurrency = watch('currency')
const isEF = entityType === 'enskild_firma'
useEffect(() => {
fetchSuppliers()
fetchAccounts()
fetchEntityType()
}, [])
// Auto-fill due date and defaults when supplier is selected
useEffect(() => {
if (watchedSupplierId) {
const supplier = suppliers.find((s) => s.id === watchedSupplierId)
if (supplier) {
const invoiceDate = watch('invoice_date')
if (invoiceDate) {
const due = new Date(invoiceDate)
due.setDate(due.getDate() + supplier.default_payment_terms)
setValue('due_date', due.toISOString().split('T')[0])
}
if (supplier.default_expense_account && fields.length > 0) {
setValue('items.0.account_number', supplier.default_expense_account)
}
if (supplier.default_currency) {
setValue('currency', supplier.default_currency)
}
if (supplier.supplier_type === 'eu_business') {
setValue('reverse_charge', true)
}
}
}
}, [watchedSupplierId, suppliers])
// Auto-select newly created supplier once it's in the list
useEffect(() => {
if (pendingSupplierSelect && suppliers.find((s) => s.id === pendingSupplierSelect)) {
setValue('supplier_id', pendingSupplierSelect, { shouldDirty: true, shouldValidate: true })
setPendingSupplierSelect(null)
}
}, [suppliers, pendingSupplierSelect, setValue])
async function fetchSuppliers() {
const res = await fetch('/api/suppliers')
const { data } = await res.json()
setSuppliers(data || [])
}
async function fetchAccounts() {
const res = await fetch('/api/bookkeeping/accounts')
const { data } = await res.json()
setAccounts(data || [])
}
async function fetchEntityType() {
try {
const res = await fetch('/api/settings')
const { data } = await res.json()
if (data?.entity_type) {
setEntityType(data.entity_type)
}
} catch {
// Default to enskild_firma
}
}
function handleAccountChange(index: number, accountNumber: string) {
setValue(`items.${index}.account_number`, accountNumber)
const currentDesc = watch(`items.${index}.description`)
if (!currentDesc && accountNumber.length === 4) {
const desc = getAccountDescription(accountNumber)
if (desc) {
setValue(`items.${index}.description`, desc.name)
}
}
}
// Calculate totals
const itemTotals = (watchedItems || []).map((item) => {
const lineTotal = Math.round((item.amount || 0) * 100) / 100
const vatAmount = Math.round(lineTotal * (item.vat_rate || 0) * 100) / 100
return { lineTotal, vatAmount }
})
const subtotal = itemTotals.reduce((sum, t) => sum + t.lineTotal, 0)
const totalVat = itemTotals.reduce((sum, t) => sum + t.vatAmount, 0)
const total = Math.round((subtotal + totalVat) * 100) / 100
async function handleCreateSupplier() {
if (!newSupplier.name.trim()) {
toast({ title: 'Namn saknas', description: 'Ange ett namn för leverantören.', variant: 'destructive' })
return
}
setIsCreatingSupplier(true)
const payload: Record<string, unknown> = {
name: newSupplier.name,
supplier_type: newSupplier.supplier_type,
}
if (newSupplier.org_number) payload.org_number = newSupplier.org_number
if (newSupplier.bankgiro) payload.bankgiro = newSupplier.bankgiro
if (newSupplier.plusgiro) payload.plusgiro = newSupplier.plusgiro
if (newSupplier.default_expense_account) payload.default_expense_account = newSupplier.default_expense_account
const res = await fetch('/api/suppliers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
const result = await res.json()
if (!res.ok) {
toast({ title: 'Kunde inte skapa leverantör', description: getErrorMessage(result, { context: 'supplier' }), variant: 'destructive' })
export default async function NewExpenseRedirectPage({
searchParams,
}: {
searchParams: Promise<SearchParams>
}) {
const params = await searchParams
const qs = new URLSearchParams()
for (const [key, value] of Object.entries(params)) {
if (value == null) continue
if (Array.isArray(value)) {
for (const v of value) qs.append(key, v)
} else {
const created = result.data as Supplier
setSuppliers((prev) => [...prev, created].sort((a, b) => a.name.localeCompare(b.name)))
setPendingSupplierSelect(created.id)
setShowNewSupplier(false)
setNewSupplier({ name: '', supplier_type: 'swedish_business', org_number: '', bankgiro: '', plusgiro: '', default_expense_account: '' })
toast({ title: 'Leverantör skapad', description: created.name })
}
setIsCreatingSupplier(false)
}
function onSubmit(data: FormData) {
if (!data.supplier_id) {
toast({ title: 'Leverantör saknas', description: 'Välj eller skapa en leverantör.', variant: 'destructive' })
return
}
if (!data.supplier_invoice_number) {
toast({ title: 'Fakturanummer saknas', description: 'Ange leverantörens fakturanummer.', variant: 'destructive' })
return
}
if (isEF) {
// EF: submit directly (auto-approve after create)
setPendingData(data)
handleDirectSubmit(data)
} else {
// AB: show review dialog first
setPendingData(data)
setShowReview(true)
qs.set(key, value)
}
}
function buildPayload(data: FormData) {
const vatTreatment = inferVatTreatment(data.items, data.reverse_charge)
return {
supplier_id: data.supplier_id,
supplier_invoice_number: data.supplier_invoice_number,
invoice_date: data.invoice_date,
due_date: data.due_date,
delivery_date: data.delivery_date || undefined,
currency: data.currency,
exchange_rate: data.exchange_rate ? parseFloat(data.exchange_rate) : undefined,
vat_treatment: vatTreatment,
reverse_charge: data.reverse_charge,
payment_reference: data.payment_reference || undefined,
notes: data.notes || undefined,
items: data.items.map((item) => ({
description: item.description,
amount: item.amount,
account_number: item.account_number,
vat_rate: item.vat_rate,
})),
}
}
// EF flow: create + auto-approve
async function handleDirectSubmit(data: FormData) {
setIsSubmitting(true)
const res = await fetch('/api/supplier-invoices', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(buildPayload(data)),
})
const result = await res.json()
if (!res.ok) {
toast({ title: 'Kunde inte registrera utgift', description: getErrorMessage(result, { context: 'supplier_invoice', statusCode: res.status }), variant: 'destructive' })
setIsSubmitting(false)
return
}
// Auto-approve for EF
const approveRes = await fetch(`/api/supplier-invoices/${result.data.id}/approve`, { method: 'POST' })
if (!approveRes.ok) {
toast({
title: 'Varning',
description: 'Utgiften skapades men kunde inte godkännas automatiskt',
variant: 'destructive',
})
router.push(`/expenses/${result.data.id}`)
} else {
toast({ title: 'Utgift registrerad', description: `Ankomstnummer: ${result.data.arrival_number}` })
router.push('/expenses')
}
setIsSubmitting(false)
}
// AB flow: create after review dialog confirmation
async function handleConfirm() {
if (!pendingData) return
setIsSubmitting(true)
const res = await fetch('/api/supplier-invoices', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(buildPayload(pendingData)),
})
const result = await res.json()
if (!res.ok) {
toast({ title: 'Kunde inte registrera utgift', description: getErrorMessage(result, { context: 'supplier_invoice', statusCode: res.status }), variant: 'destructive' })
} else {
toast({ title: 'Utgift registrerad', description: `Ankomstnummer: ${result.data.arrival_number}` })
setShowReview(false)
router.push(`/expenses/${result.data.id}`)
}
setIsSubmitting(false)
}
return (
<div className="space-y-6 max-w-4xl">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => router.push('/expenses')}>
<ArrowLeft className="h-5 w-5" />
</Button>
<div>
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Ny utgift</h1>
<p className="text-muted-foreground">
Registrera en inkommande faktura
</p>
</div>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Section 1: Faktura */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Faktura</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Leverantör *</Label>
<Controller
name="supplier_id"
control={control}
render={({ field }) => (
<Select
value={field.value}
onValueChange={(v) => {
if (v === '__new__') {
setShowNewSupplier(true)
} else {
field.onChange(v)
}
}}
>
<SelectTrigger>
<SelectValue placeholder="Välj leverantör" />
</SelectTrigger>
<SelectContent>
{suppliers.map((s) => (
<SelectItem key={s.id} value={s.id}>{s.name}</SelectItem>
))}
<SelectItem value="__new__" className="text-primary font-medium">
+ Lägg till ny leverantör...
</SelectItem>
</SelectContent>
</Select>
)}
/>
</div>
<div className="space-y-2">
<Label>Leverantörens fakturanummer *</Label>
<Input
placeholder="Fakturanr från leverantören"
{...register('supplier_invoice_number')}
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Fakturadatum *</Label>
<Input type="date" {...register('invoice_date')} />
</div>
<div className="space-y-2">
<Label>Förfallodatum *</Label>
<Input type="date" {...register('due_date')} />
</div>
<div className="space-y-2">
<Label>OCR / Betalningsreferens</Label>
<Input
placeholder="OCR-nummer"
{...register('payment_reference')}
/>
</div>
</div>
</CardContent>
</Card>
{/* Section 2: Kontering */}
<Card>
<CardHeader className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<CardTitle className="text-lg">Kontering</CardTitle>
<Button
type="button"
variant="outline"
size="sm"
className="w-full sm:w-auto"
onClick={() =>
append({ description: '', amount: 0, account_number: '', vat_rate: 0.25 })
}
>
<Plus className="mr-2 h-4 w-4" />
Lägg till rad
</Button>
</CardHeader>
<CardContent>
{/* Desktop: table layout */}
<div className="hidden sm:block">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-2 w-28">Konto</th>
<th className="pb-2">Beskrivning</th>
<th className="pb-2 w-32">Belopp (exkl.)</th>
<th className="pb-2 w-24">Momssats</th>
<th className="pb-2 w-24 text-right">Moms</th>
<th className="pb-2 w-8"></th>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.id} className="border-b last:border-0 align-top">
<td className="py-2 pr-2">
<Controller
name={`items.${index}.account_number`}
control={control}
render={({ field: f }) => (
<AccountCombobox
value={f.value}
accounts={accounts}
onChange={(val) => handleAccountChange(index, val)}
/>
)}
/>
</td>
<td className="py-2 pr-2">
<Controller
name={`items.${index}.description`}
control={control}
render={({ field }) => (
<Input
placeholder="Beskrivning"
value={field.value || ''}
onChange={field.onChange}
onBlur={field.onBlur}
/>
)}
/>
</td>
<td className="py-2 pr-2">
<Controller
name={`items.${index}.amount`}
control={control}
render={({ field }) => (
<Input
type="number"
step="0.01"
placeholder="0,00"
value={field.value || ''}
onChange={(e) => {
const parsed = parseFloat(e.target.value)
field.onChange(e.target.value === '' || isNaN(parsed) ? 0 : parsed)
}}
/>
)}
/>
</td>
<td className="py-2 pr-2">
<Controller
name={`items.${index}.vat_rate`}
control={control}
render={({ field: f }) => (
<Select
value={String(f.value)}
onValueChange={(v) => f.onChange(parseFloat(v))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0.25">25%</SelectItem>
<SelectItem value="0.12">12%</SelectItem>
<SelectItem value="0.06">6%</SelectItem>
<SelectItem value="0">0%</SelectItem>
</SelectContent>
</Select>
)}
/>
</td>
<td className="py-2 pr-2 text-right font-mono pt-4">
{formatAmount(itemTotals[index]?.vatAmount || 0)}
</td>
<td className="py-2 pt-3">
{fields.length > 1 && (
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile: stacked card layout */}
<div className="sm:hidden space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="border rounded-lg p-3 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">Rad {index + 1}</span>
{fields.length > 1 && (
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
)}
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Konto</Label>
<Controller
name={`items.${index}.account_number`}
control={control}
render={({ field: f }) => (
<AccountCombobox
value={f.value}
accounts={accounts}
onChange={(val) => handleAccountChange(index, val)}
/>
)}
/>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Beskrivning</Label>
<Controller
name={`items.${index}.description`}
control={control}
render={({ field }) => (
<Input
placeholder="Beskrivning"
value={field.value || ''}
onChange={field.onChange}
/>
)}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Belopp (exkl.)</Label>
<Controller
name={`items.${index}.amount`}
control={control}
render={({ field }) => (
<Input
type="number"
step="0.01"
placeholder="0,00"
value={field.value || ''}
onChange={(e) => field.onChange(e.target.value === '' ? 0 : parseFloat(e.target.value) || 0)}
/>
)}
/>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Momssats</Label>
<Controller
name={`items.${index}.vat_rate`}
control={control}
render={({ field: f }) => (
<Select
value={String(f.value)}
onValueChange={(v) => f.onChange(parseFloat(v))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0.25">25%</SelectItem>
<SelectItem value="0.12">12%</SelectItem>
<SelectItem value="0.06">6%</SelectItem>
<SelectItem value="0">0%</SelectItem>
</SelectContent>
</Select>
)}
/>
</div>
</div>
<div className="flex justify-between items-center pt-1 border-t">
<span className="text-xs text-muted-foreground">Moms</span>
<span className="font-mono text-sm">{formatAmount(itemTotals[index]?.vatAmount || 0)} kr</span>
</div>
</div>
))}
</div>
{/* Totals */}
<div className="mt-4 pt-4 border-t space-y-2">
<div className="flex justify-between sm:justify-end sm:gap-8">
<span className="text-muted-foreground">Netto (exkl. moms)</span>
<span className="font-mono sm:w-32 text-right">{formatAmount(subtotal)} kr</span>
</div>
<div className="flex justify-between sm:justify-end sm:gap-8">
<span className="text-muted-foreground">Moms</span>
<span className="font-mono sm:w-32 text-right">{formatAmount(totalVat)} kr</span>
</div>
<div className="flex justify-between sm:justify-end sm:gap-8 font-bold text-lg">
<span>Totalt</span>
<span className="font-mono sm:w-32 text-right">{formatAmount(total)} kr</span>
</div>
</div>
</CardContent>
</Card>
{/* Section 3: Övrigt (collapsible) */}
<Card>
<CardHeader
className="cursor-pointer hover:bg-muted/30 transition-colors"
onClick={() => setAdvancedOpen(!advancedOpen)}
>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">Övrigt</CardTitle>
<ChevronDown className={`h-4 w-4 text-muted-foreground transition-transform ${advancedOpen ? 'rotate-180' : ''}`} />
</div>
</CardHeader>
{advancedOpen && (
<CardContent className="space-y-4 pt-0">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Valuta</Label>
<Controller
name="currency"
control={control}
render={({ field }) => (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="SEK">SEK</SelectItem>
<SelectItem value="EUR">EUR</SelectItem>
<SelectItem value="USD">USD</SelectItem>
<SelectItem value="GBP">GBP</SelectItem>
<SelectItem value="NOK">NOK</SelectItem>
<SelectItem value="DKK">DKK</SelectItem>
</SelectContent>
</Select>
)}
/>
</div>
{watchedCurrency !== 'SEK' && (
<div className="space-y-2">
<Label>Växelkurs</Label>
<Input
type="number"
step="0.0001"
placeholder="1.0000"
{...register('exchange_rate')}
/>
</div>
)}
</div>
<div className="space-y-2">
<Label>Leveransdatum (ML krav)</Label>
<Input type="date" {...register('delivery_date')} />
</div>
<div className="flex items-center gap-2">
<Controller
name="reverse_charge"
control={control}
render={({ field }) => (
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
)}
/>
<Label>Omvänd skattskyldighet (reverse charge)</Label>
</div>
<div className="space-y-2">
<Label>Anteckningar</Label>
<Textarea
placeholder="Interna anteckningar om denna utgift..."
{...register('notes')}
/>
</div>
</CardContent>
)}
</Card>
{/* Submit */}
<div className="flex flex-col-reverse sm:flex-row sm:justify-end gap-3 sm:gap-4">
<Button type="button" variant="outline" className="w-full sm:w-auto" onClick={() => router.push('/expenses')}>
Avbryt
</Button>
<Button
type="submit"
disabled={isSubmitting || !canWrite}
className="w-full sm:w-auto"
title={!canWrite ? 'Du har endast läsbehörighet i detta företag' : undefined}
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Registrerar...
</>
) : !canWrite ? (
<>
<Lock className="mr-2 h-4 w-4" />
{isEF ? 'Registrera utgift' : 'Granska & registrera'}
</>
) : isEF ? (
'Registrera utgift'
) : (
'Granska & registrera'
)}
</Button>
</div>
</form>
{/* Review dialog (AB only) */}
{pendingData && !isEF && (() => {
const selectedSupplier = suppliers.find((s) => s.id === pendingData.supplier_id)
if (!selectedSupplier) return null
return (
<ConfirmationDialog
open={showReview}
onOpenChange={setShowReview}
onConfirm={handleConfirm}
isSubmitting={isSubmitting}
title="Granska utgift"
warningText="Utgiften registreras och en verifikation bokförs. Verifikationen kan inte redigeras direkt, men kan korrigeras via en ändringsverifikation."
confirmLabel="Bekräfta & registrera"
>
<SupplierInvoiceReviewContent
supplier={selectedSupplier}
invoiceNumber={pendingData.supplier_invoice_number}
invoiceDate={pendingData.invoice_date}
dueDate={pendingData.due_date}
deliveryDate={pendingData.delivery_date || undefined}
currency={pendingData.currency}
exchangeRate={pendingData.exchange_rate || undefined}
reverseCharge={pendingData.reverse_charge}
paymentReference={pendingData.payment_reference || undefined}
items={pendingData.items}
subtotal={subtotal}
totalVat={totalVat}
total={total}
/>
</ConfirmationDialog>
)
})()}
{/* New Supplier Dialog */}
<Dialog open={showNewSupplier} onOpenChange={setShowNewSupplier}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Ny leverantör</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>Namn *</Label>
<Input
placeholder="Leverantörens namn"
value={newSupplier.name}
onChange={(e) => setNewSupplier((p) => ({ ...p, name: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label>Typ</Label>
<Select
value={newSupplier.supplier_type}
onValueChange={(v) => setNewSupplier((p) => ({ ...p, supplier_type: v }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="swedish_business">Svenskt företag</SelectItem>
<SelectItem value="eu_business">EU-företag</SelectItem>
<SelectItem value="non_eu_business">Utomeuropeiskt</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Organisationsnummer</Label>
<Input
placeholder="XXXXXX-XXXX"
value={newSupplier.org_number}
onChange={(e) => setNewSupplier((p) => ({ ...p, org_number: e.target.value }))}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Bankgiro</Label>
<Input
placeholder="XXX-XXXX"
value={newSupplier.bankgiro}
onChange={(e) => setNewSupplier((p) => ({ ...p, bankgiro: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label>Plusgiro</Label>
<Input
placeholder="XXXXXX-X"
value={newSupplier.plusgiro}
onChange={(e) => setNewSupplier((p) => ({ ...p, plusgiro: e.target.value }))}
/>
</div>
</div>
<div className="space-y-2">
<Label>Standardkonto (kostnad)</Label>
<Input
placeholder="t.ex. 5010"
value={newSupplier.default_expense_account}
onChange={(e) => setNewSupplier((p) => ({ ...p, default_expense_account: e.target.value }))}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowNewSupplier(false)}>
Avbryt
</Button>
<Button onClick={handleCreateSupplier} disabled={isCreatingSupplier}>
{isCreatingSupplier ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Skapar...
</>
) : (
'Skapa leverantör'
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
const suffix = qs.toString()
redirect(`/supplier-invoices/new${suffix ? `?${suffix}` : ''}`)
}
+3 -252
View File
@@ -1,254 +1,5 @@
'use client'
import { redirect } from 'next/navigation'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { createClient } from '@/lib/supabase/client'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { PageHeader } from '@/components/ui/page-header'
import { EmptyState } from '@/components/ui/empty-state'
import { useToast } from '@/components/ui/use-toast'
import { formatCurrency } from '@/lib/utils'
import { Plus, Search, Wallet, Lock } from 'lucide-react'
import { useCompany } from '@/contexts/CompanyContext'
import { useCanWrite } from '@/lib/hooks/use-can-write'
import type { SupplierInvoice } from '@/types'
type ExpenseInvoice = SupplierInvoice & { supplier?: { id: string; name: string } }
const UNPAID_STATUSES = ['registered', 'approved', 'overdue', 'partially_paid']
const PAID_STATUSES = ['paid', 'credited']
function getSimplifiedStatus(status: string): { label: string; variant: 'default' | 'secondary' | 'destructive'; borderColor: string } {
switch (status) {
case 'registered':
case 'approved':
return { label: 'Obetald', variant: 'default', borderColor: 'border-l-warning' }
case 'overdue':
return { label: 'Förfallen', variant: 'destructive', borderColor: 'border-l-destructive' }
case 'partially_paid':
return { label: 'Delbetald', variant: 'default', borderColor: 'border-l-orange-400' }
case 'paid':
return { label: 'Betald', variant: 'secondary', borderColor: 'border-l-success' }
case 'credited':
return { label: 'Krediterad', variant: 'secondary', borderColor: 'border-l-muted-foreground/30' }
default:
return { label: status, variant: 'secondary', borderColor: 'border-l-muted-foreground/30' }
}
}
function getRelativeTimeLabel(dueDateStr: string, status: string): { text: string; color: string } | null {
if (PAID_STATUSES.includes(status)) return null
const today = new Date()
today.setHours(0, 0, 0, 0)
const dueDate = new Date(dueDateStr)
dueDate.setHours(0, 0, 0, 0)
const diffDays = Math.round((dueDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
if (diffDays < 0) {
return { text: `${Math.abs(diffDays)} dagar försenad`, color: 'text-destructive' }
} else if (diffDays === 0) {
return { text: 'Förfaller idag', color: 'text-warning-foreground' }
} else if (diffDays <= 3) {
return { text: `${diffDays} dagar kvar`, color: 'text-warning-foreground' }
} else if (diffDays <= 7) {
return { text: `${diffDays} dagar kvar`, color: 'text-muted-foreground' }
}
return null
}
export default function ExpensesPage() {
const { company } = useCompany()
const { canWrite } = useCanWrite()
const [invoices, setInvoices] = useState<ExpenseInvoice[]>([])
const [isLoading, setIsLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
const [activeTab, setActiveTab] = useState('unpaid')
const { toast } = useToast()
const supabase = createClient()
async function fetchExpenses() {
if (!company) return
setIsLoading(true)
const { data, error } = await supabase
.from('supplier_invoices')
.select('*, supplier:suppliers(id, name)')
.eq('company_id', company.id)
.order('due_date', { ascending: true })
if (error) {
toast({ title: 'Kunde inte ladda utgifter', description: 'Kontrollera din anslutning och försök igen.', variant: 'destructive' })
} else {
setInvoices(data || [])
}
setIsLoading(false)
}
useEffect(() => {
fetchExpenses()
}, [])
const filteredInvoices = invoices.filter((inv) => {
const matchesSearch =
(inv.supplier?.name || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
inv.supplier_invoice_number.toLowerCase().includes(searchTerm.toLowerCase())
const matchesTab =
activeTab === 'unpaid'
? UNPAID_STATUSES.includes(inv.status)
: PAID_STATUSES.includes(inv.status)
return matchesSearch && matchesTab
})
return (
<div className="space-y-6">
<PageHeader
title="Leverantörsfakturor"
description="Registrera och hantera dina utgifter"
action={
canWrite ? (
<Link href="/expenses/new">
<Button>
<Plus className="mr-2 h-4 w-4" />
Ny utgift
</Button>
</Link>
) : (
<Button
disabled
title="Du har endast läsbehörighet i detta företag"
>
<Lock className="mr-2 h-4 w-4" />
Ny utgift
</Button>
)
}
/>
{/* Search and tabs */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Sök på leverantör eller fakturanummer..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="unpaid">Att betala</TabsTrigger>
<TabsTrigger value="paid">Betalda</TabsTrigger>
</TabsList>
</Tabs>
</div>
{/* Expense list */}
{isLoading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<Card key={i} className="animate-pulse">
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div className="space-y-2">
<div className="h-5 bg-muted rounded w-32" />
<div className="h-4 bg-muted rounded w-48" />
</div>
<div className="h-8 bg-muted rounded w-24" />
</div>
</CardContent>
</Card>
))}
</div>
) : filteredInvoices.length === 0 ? (
<Card>
<CardContent>
{searchTerm ? (
<div className="flex flex-col items-center justify-center py-12">
<Wallet className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium">Inga träffar</h3>
<p className="text-muted-foreground text-center mt-1">
Inga utgifter matchar &quot;{searchTerm}&quot;
</p>
</div>
) : invoices.length === 0 ? (
<EmptyState
icon={Wallet}
title="Inga utgifter ännu"
description="Registrera din första utgift. Vi bokför automatiskt och håller koll på betalningar."
actionLabel="Ny utgift"
actionHref="/expenses/new"
/>
) : (
<div className="flex flex-col items-center justify-center py-12">
<Wallet className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium">Inga utgifter i denna kategori</h3>
<p className="text-muted-foreground text-center mt-1">
Prova att byta flik för att se fler utgifter
</p>
</div>
)}
</CardContent>
</Card>
) : (
<div className="space-y-2">
{filteredInvoices.map((inv) => {
const status = getSimplifiedStatus(inv.status)
const relativeTime = inv.due_date ? getRelativeTimeLabel(inv.due_date, inv.status) : null
return (
<Link key={inv.id} href={`/expenses/${inv.id}`}>
<Card className={`hover:border-primary/50 transition-colors cursor-pointer border-l-4 ${status.borderColor} ${inv.status === 'overdue' ? 'ring-1 ring-destructive/20' : ''}`}>
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center">
<Wallet className="h-5 w-5 text-muted-foreground" />
</div>
<div>
<div className="flex items-center gap-2">
<p className="font-medium">{inv.supplier?.name || 'Okänd leverantör'}</p>
<Badge variant={status.variant as 'default' | 'secondary' | 'destructive'}>
{status.label}
</Badge>
</div>
<div className="flex items-center gap-2">
<p className="text-sm text-muted-foreground">
{inv.supplier_invoice_number} · Förfaller {inv.due_date}
</p>
{relativeTime && (
<span className={`text-xs font-medium ${relativeTime.color}`}>
{relativeTime.text}
</span>
)}
</div>
</div>
</div>
<div className="text-right">
<p className="font-medium tabular-nums">
{formatCurrency(inv.total, inv.currency)}
</p>
{inv.remaining_amount !== inv.total && inv.remaining_amount > 0 && (
<p className="text-sm text-muted-foreground tabular-nums">
Kvar: {formatCurrency(inv.remaining_amount, inv.currency)}
</p>
)}
</div>
</div>
</CardContent>
</Card>
</Link>
)
})}
</div>
)}
</div>
)
export default function ExpensesRedirectPage() {
redirect('/supplier-invoices')
}
File diff suppressed because it is too large Load Diff
+106 -45
View File
@@ -33,9 +33,30 @@ import type { TransactionWithInvoice, ViewMode, CategorizeHandler } from '@/comp
import { useCompany } from '@/contexts/CompanyContext'
import { getErrorMessage } from '@/lib/errors/get-error-message'
import { formatCurrency, formatDate } from '@/lib/utils'
import type { TransactionCategory, CreateTransactionInput, Invoice, Customer, VatTreatment, EntityType, LinePatternEntry } from '@/types'
import type { TransactionCategory, CreateTransactionInput, Invoice, Customer, SupplierInvoice, Supplier, VatTreatment, EntityType, LinePatternEntry } from '@/types'
import type { SuggestedCategory, SuggestedTemplate } from '@/lib/transactions/category-suggestions'
type InvoiceWithCustomer = Invoice & { customer?: Customer }
type SupplierInvoiceWithSupplier = SupplierInvoice & { supplier?: Supplier }
function buildInvoiceMap(rows: InvoiceWithCustomer[] | null): Record<string, InvoiceWithCustomer> {
if (!rows) return {}
return rows.reduce<Record<string, InvoiceWithCustomer>>((acc, inv) => {
acc[inv.id] = inv
return acc
}, {})
}
function buildSupplierInvoiceMap(
rows: SupplierInvoiceWithSupplier[] | null,
): Record<string, SupplierInvoiceWithSupplier> {
if (!rows) return {}
return rows.reduce<Record<string, SupplierInvoiceWithSupplier>>((acc, inv) => {
acc[inv.id] = inv
return acc
}, {})
}
interface QuickReviewState {
transaction: TransactionWithInvoice
category: TransactionCategory
@@ -107,12 +128,16 @@ export default function TransactionsPage() {
const uncategorizedTransactions = transactions
.filter((t) => t.is_business === null && !exitingIds.has(t.id))
.sort((a, b) => {
const aHasMatch = a.potential_invoice ? 1 : 0
const bHasMatch = b.potential_invoice ? 1 : 0
const aHasMatch = a.potential_invoice || a.potential_supplier_invoice ? 1 : 0
const bHasMatch = b.potential_invoice || b.potential_supplier_invoice ? 1 : 0
if (aHasMatch !== bHasMatch) return bHasMatch - aHasMatch
return b.date.localeCompare(a.date)
})
const transactionsWithMatches = transactions.filter((t) => t.potential_invoice && !t.invoice_id)
const transactionsWithMatches = transactions.filter(
(t) =>
(t.potential_invoice && !t.invoice_id) ||
(t.potential_supplier_invoice && !t.supplier_invoice_id),
)
const PAGE_SIZE = 200
@@ -139,30 +164,37 @@ export default function TransactionsPage() {
return
}
const potentialInvoiceIds = (txData || [])
const rows = txData || []
const potentialInvoiceIds = rows
.filter((t) => t.potential_invoice_id)
.map((t) => t.potential_invoice_id)
const potentialSupplierInvoiceIds = rows
.filter((t) => t.potential_supplier_invoice_id)
.map((t) => t.potential_supplier_invoice_id)
const invoiceResult = potentialInvoiceIds.length > 0
? await supabase.from('invoices').select('*, customer:customers(*)').in('id', potentialInvoiceIds)
: { data: null }
const [invoiceResult, supplierInvoiceResult] = await Promise.all([
potentialInvoiceIds.length > 0
? supabase.from('invoices').select('*, customer:customers(*)').in('id', potentialInvoiceIds)
: Promise.resolve({ data: null }),
potentialSupplierInvoiceIds.length > 0
? supabase.from('supplier_invoices').select('*, supplier:suppliers(*)').in('id', potentialSupplierInvoiceIds)
: Promise.resolve({ data: null }),
])
let invoiceMap: Record<string, Invoice & { customer?: Customer }> = {}
if (invoiceResult.data) {
invoiceMap = invoiceResult.data.reduce((acc, inv) => {
acc[inv.id] = inv
return acc
}, {} as Record<string, Invoice & { customer?: Customer }>)
}
const invoiceMap = buildInvoiceMap(invoiceResult.data)
const supplierInvoiceMap = buildSupplierInvoiceMap(supplierInvoiceResult.data)
const transactionsWithInvoices: TransactionWithInvoice[] = (txData || []).map((t) => ({
const transactionsWithInvoices: TransactionWithInvoice[] = rows.map((t) => ({
...t,
potential_invoice: t.potential_invoice_id ? invoiceMap[t.potential_invoice_id] : undefined,
potential_supplier_invoice: t.potential_supplier_invoice_id
? supplierInvoiceMap[t.potential_supplier_invoice_id]
: undefined,
}))
setTransactions(transactionsWithInvoices)
setTotalUncategorizedCount(uncatCount ?? 0)
setHasMore((txData || []).length >= PAGE_SIZE)
setHasMore(rows.length >= PAGE_SIZE)
setIsLoading(false)
}
@@ -187,22 +219,28 @@ export default function TransactionsPage() {
const potentialInvoiceIds = txData
.filter((t) => t.potential_invoice_id)
.map((t) => t.potential_invoice_id)
const potentialSupplierInvoiceIds = txData
.filter((t) => t.potential_supplier_invoice_id)
.map((t) => t.potential_supplier_invoice_id)
const invoiceResult = potentialInvoiceIds.length > 0
? await supabase.from('invoices').select('*, customer:customers(*)').in('id', potentialInvoiceIds)
: { data: null }
const [invoiceResult, supplierInvoiceResult] = await Promise.all([
potentialInvoiceIds.length > 0
? supabase.from('invoices').select('*, customer:customers(*)').in('id', potentialInvoiceIds)
: Promise.resolve({ data: null }),
potentialSupplierInvoiceIds.length > 0
? supabase.from('supplier_invoices').select('*, supplier:suppliers(*)').in('id', potentialSupplierInvoiceIds)
: Promise.resolve({ data: null }),
])
let invoiceMap: Record<string, Invoice & { customer?: Customer }> = {}
if (invoiceResult.data) {
invoiceMap = invoiceResult.data.reduce((acc, inv) => {
acc[inv.id] = inv
return acc
}, {} as Record<string, Invoice & { customer?: Customer }>)
}
const invoiceMap = buildInvoiceMap(invoiceResult.data)
const supplierInvoiceMap = buildSupplierInvoiceMap(supplierInvoiceResult.data)
const newTransactions: TransactionWithInvoice[] = txData.map((t) => ({
...t,
potential_invoice: t.potential_invoice_id ? invoiceMap[t.potential_invoice_id] : undefined,
potential_supplier_invoice: t.potential_supplier_invoice_id
? supplierInvoiceMap[t.potential_supplier_invoice_id]
: undefined,
}))
setTransactions((prev) => [...prev, ...newTransactions])
@@ -369,26 +407,41 @@ export default function TransactionsPage() {
}
async function handleConfirmInvoiceMatch() {
if (!selectedTransaction?.potential_invoice) return
if (!selectedTransaction) return
const isSupplier = !!selectedTransaction.potential_supplier_invoice
const isCustomer = !!selectedTransaction.potential_invoice
if (!isSupplier && !isCustomer) return
setIsConfirmingMatch(true)
try {
const response = await fetch(`/api/transactions/${selectedTransaction.id}/match-invoice`, {
const url = isSupplier
? `/api/transactions/${selectedTransaction.id}/match-supplier-invoice`
: `/api/transactions/${selectedTransaction.id}/match-invoice`
const body = isSupplier
? { supplier_invoice_id: selectedTransaction.potential_supplier_invoice!.id }
: { invoice_id: selectedTransaction.potential_invoice!.id }
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ invoice_id: selectedTransaction.potential_invoice.id }),
body: JSON.stringify(body),
})
const result = await response.json()
if (!response.ok) {
toast({ title: 'Fakturamatchning misslyckades', description: getErrorMessage(result, { context: 'transaction' }), variant: 'destructive' })
toast({
title: isSupplier ? 'Leverantörsfakturamatchning misslyckades' : 'Fakturamatchning misslyckades',
description: getErrorMessage(result, { context: 'transaction' }),
variant: 'destructive',
})
setIsConfirmingMatch(false)
return
}
toast({
title: 'Faktura matchad',
description: `Faktura ${selectedTransaction.potential_invoice.invoice_number} markerad som betald`,
})
const label = isSupplier
? `Leverantörsfaktura ${selectedTransaction.potential_supplier_invoice!.supplier_invoice_number} markerad som betald`
: `Faktura ${selectedTransaction.potential_invoice!.invoice_number} markerad som betald`
toast({ title: isSupplier ? 'Leverantörsfaktura matchad' : 'Faktura matchad', description: label })
setMatchDialogOpen(false)
// Mark as exiting for animation
@@ -397,15 +450,23 @@ export default function TransactionsPage() {
setTransactions((prev) =>
prev.map((t) =>
t.id === selectedTransaction.id
? {
...t,
invoice_id: selectedTransaction.potential_invoice?.id || null,
potential_invoice_id: null,
potential_invoice: undefined,
is_business: true,
category: 'income_services' as TransactionCategory,
journal_entry_id: result.journal_entry_id,
}
? isSupplier
? {
...t,
supplier_invoice_id: selectedTransaction.potential_supplier_invoice?.id || null,
potential_supplier_invoice: undefined,
is_business: true,
journal_entry_id: result.journal_entry_id,
}
: {
...t,
invoice_id: selectedTransaction.potential_invoice?.id || null,
potential_invoice_id: null,
potential_invoice: undefined,
is_business: true,
category: 'income_services' as TransactionCategory,
journal_entry_id: result.journal_entry_id,
}
: t
)
)
@@ -418,7 +479,7 @@ export default function TransactionsPage() {
setIsConfirmingMatch(false)
}, 350)
} catch {
toast({ title: 'Matchning misslyckades', description: 'Transaktionen kunde inte matchas med fakturan. Försök igen.', variant: 'destructive' })
toast({ title: 'Matchning misslyckades', description: 'Transaktionen kunde inte matchas. Försök igen.', variant: 'destructive' })
setIsConfirmingMatch(false)
}
}
+1 -3
View File
@@ -22,7 +22,6 @@ import {
HelpCircle,
ChevronDown,
Building2,
FileInput,
Wallet,
TrendingUp,
ClipboardCheck,
@@ -71,10 +70,9 @@ const navItems: NavItem[] = [
{ href: '/invoices', label: 'Fakturor', icon: Receipt, group: 'försäljning' },
{ href: '/customers', label: 'Kunder', icon: Users, group: 'försäljning' },
// AP — Accounts Payable
{ href: '/expenses', label: 'Leverantörsfakturor', icon: Wallet, group: 'inköp' },
{ href: '/supplier-invoices', label: 'Leverantörsfakturor', icon: Wallet, group: 'inköp' },
// Temporarily hidden pending module rework (see feedback #49)
{ href: '/suppliers', label: 'Leverantörer', icon: Building2, group: 'inköp', hidden: true },
{ href: '/supplier-invoices', label: 'Leverantörsfakturor', icon: FileInput, group: 'inköp', hidden: true },
// General accounting
{ href: '/pending', label: 'Granskning', icon: ClipboardCheck, group: 'redovisning' },
{ href: '/transactions', label: 'Transaktioner', icon: ArrowLeftRight, group: 'redovisning' },
@@ -0,0 +1,178 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
// `createClient()` returns a fresh object on every call, so we keep the
// instance creation inside the effect — listing it as a dep would re-fire
// the fetch on every render and create an infinite loop.
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
import { useCompany } from '@/contexts/CompanyContext'
import { formatCurrency, formatDate } from '@/lib/utils'
import { Loader2, Search, ArrowDownRight } from 'lucide-react'
interface BankTransaction {
id: string
date: string
description: string
amount: number
currency: string
}
interface BankTransactionPickerProps {
open: boolean
onOpenChange: (open: boolean) => void
/** Invoice total in invoice currency. Used to rank transactions by amount proximity. */
targetAmount: number
/** Invoice currency. Used to filter the candidate list when set. */
targetCurrency: string
onPick: (transactionId: string) => void
}
/**
* Lightweight picker for unmatched outgoing bank transactions. Used by
* `/supplier-invoices/new` when the user wants to register an invoice and
* mark it paid against a specific bank transaction in one go.
*
* Filters: negative amount, no journal entry, no supplier_invoice link.
* Ranks by absolute amount-difference vs `targetAmount`.
*/
export default function BankTransactionPicker({
open,
onOpenChange,
targetAmount,
targetCurrency,
onPick,
}: BankTransactionPickerProps) {
const { company } = useCompany()
const [transactions, setTransactions] = useState<BankTransaction[]>([])
const [isLoading, setIsLoading] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
useEffect(() => {
if (!open || !company) return
let cancelled = false
const supabase = createClient()
;(async () => {
setIsLoading(true)
const query = supabase
.from('transactions')
.select('id, date, description, amount, currency')
.eq('company_id', company.id)
.lt('amount', 0)
.is('journal_entry_id', null)
.is('supplier_invoice_id', null)
.is('invoice_id', null)
.order('date', { ascending: false })
.limit(200)
const { data, error } = targetCurrency
? await query.eq('currency', targetCurrency)
: await query
if (!cancelled) {
if (!error) setTransactions(data || [])
setIsLoading(false)
}
})()
return () => {
cancelled = true
}
}, [open, company, targetCurrency])
const filtered = useMemo(() => {
const term = searchTerm.trim().toLowerCase()
const matches = transactions.filter((t) =>
term === '' ? true : (t.description || '').toLowerCase().includes(term),
)
// Rank by closeness to target (treat target as the absolute outflow)
return matches.sort((a, b) => {
const da = Math.abs(Math.abs(a.amount) - targetAmount)
const db = Math.abs(Math.abs(b.amount) - targetAmount)
return da - db
})
}, [transactions, searchTerm, targetAmount])
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-2xl max-h-[90vh] flex flex-col">
<DialogHeader>
<DialogTitle>Välj banktransaktion</DialogTitle>
<DialogDescription>
Välj den utgående banktransaktion som motsvarar denna faktura. Fakturan registreras och markeras som betald i ett steg.
</DialogDescription>
</DialogHeader>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
autoFocus
placeholder="Sök på beskrivning..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<div className="flex-1 overflow-y-auto space-y-2 -mx-1 px-1">
{isLoading ? (
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Laddar transaktioner…
</div>
) : filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-sm text-muted-foreground">
<ArrowDownRight className="h-8 w-8 mb-2" />
{transactions.length === 0
? 'Inga okategoriserade utgifts­transaktioner hittades.'
: `Inga transaktioner matchar "${searchTerm}".`}
</div>
) : (
filtered.map((tx) => {
const absAmount = Math.abs(tx.amount)
const diff = Math.abs(absAmount - targetAmount)
const isExact = diff < 0.01
return (
<button
key={tx.id}
type="button"
onClick={() => onPick(tx.id)}
className="w-full text-left rounded-lg border p-3 hover:border-primary/50 hover:bg-muted/30 transition-colors"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="font-medium truncate">{tx.description}</p>
<p className="text-xs text-muted-foreground">{formatDate(tx.date)}</p>
</div>
<div className="text-right shrink-0">
<p className="font-mono font-medium tabular-nums text-destructive">
{formatCurrency(tx.amount, tx.currency)}
</p>
{isExact ? (
<p className="text-xs text-success">Belopp matchar</p>
) : targetAmount > 0 ? (
<p className="text-xs text-muted-foreground">
Diff {formatCurrency(diff, tx.currency)}
</p>
) : null}
</div>
</div>
</button>
)
})
)}
</div>
<div className="flex justify-end pt-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Avbryt
</Button>
</div>
</DialogContent>
</Dialog>
)
}
+78 -43
View File
@@ -21,17 +21,27 @@ export default function InvoiceMatchDialog({
isConfirming,
onConfirm,
}: InvoiceMatchDialogProps) {
const isSupplierInvoice = !!transaction?.potential_supplier_invoice
const isCustomerInvoice = !!transaction?.potential_invoice
// The invoice candidate the dialog is about, normalized to a single shape.
// Supplier invoices show the negative-amount paid-out match; customer
// invoices show the positive-amount paid-in match. Each side carries its
// own follow-up action language.
const matchTitle = isSupplierInvoice ? 'Bekräfta leverantörsfakturamatchning' : 'Bekräfta fakturamatchning'
const matchDescription = isSupplierInvoice
? 'Vill du koppla denna transaktion till leverantörsfakturan? Fakturan kommer att markeras som betald och en betalningsverifikation skapas.'
: 'Vill du koppla denna transaktion till fakturan? Fakturan kommer att markeras som betald.'
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Bekräfta fakturamatchning</DialogTitle>
<DialogDescription>
Vill du koppla denna transaktion till fakturan? Fakturan kommer att markeras som betald.
</DialogDescription>
<DialogTitle>{matchTitle}</DialogTitle>
<DialogDescription>{matchDescription}</DialogDescription>
</DialogHeader>
{transaction?.potential_invoice && (
{transaction && (isCustomerInvoice || isSupplierInvoice) && (
<div className="space-y-4">
{/* Transaction details */}
<div className="rounded-lg border p-4 space-y-2">
@@ -39,51 +49,82 @@ export default function InvoiceMatchDialog({
<p className="font-medium">{transaction.description}</p>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">{formatDate(transaction.date)}</span>
<span className="font-medium text-success">
+{formatCurrency(transaction.amount, transaction.currency)}
<span className={`font-medium ${transaction.amount > 0 ? 'text-success' : ''}`}>
{transaction.amount > 0 ? '+' : ''}
{formatCurrency(transaction.amount, transaction.currency)}
</span>
</div>
</div>
{/* Invoice details */}
<div className="rounded-lg border p-4 space-y-2">
<p className="text-sm font-medium text-muted-foreground">Faktura</p>
<p className="font-medium">
Faktura {transaction.potential_invoice.invoice_number}
</p>
<p className="text-sm text-muted-foreground">
{transaction.potential_invoice.customer?.name || 'Okänd kund'}
</p>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">
Förfaller: {formatDate(transaction.potential_invoice.due_date)}
</span>
<span className="font-medium">
{formatCurrency(
transaction.potential_invoice.total,
transaction.potential_invoice.currency
)}
</span>
{isCustomerInvoice && (
<div className="rounded-lg border p-4 space-y-2">
<p className="text-sm font-medium text-muted-foreground">Faktura</p>
<p className="font-medium">
Faktura {transaction.potential_invoice!.invoice_number}
</p>
<p className="text-sm text-muted-foreground">
{transaction.potential_invoice!.customer?.name || 'Okänd kund'}
</p>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">
Förfaller: {formatDate(transaction.potential_invoice!.due_date)}
</span>
<span className="font-medium">
{formatCurrency(
transaction.potential_invoice!.total,
transaction.potential_invoice!.currency,
)}
</span>
</div>
</div>
</div>
)}
{isSupplierInvoice && (
<div className="rounded-lg border p-4 space-y-2">
<p className="text-sm font-medium text-muted-foreground">Leverantörsfaktura</p>
<p className="font-medium">
Faktura {transaction.potential_supplier_invoice!.supplier_invoice_number}
</p>
<p className="text-sm text-muted-foreground">
Ankomstnr: {transaction.potential_supplier_invoice!.arrival_number}
</p>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">
Förfaller: {formatDate(transaction.potential_supplier_invoice!.due_date)}
</span>
<span className="font-medium">
{formatCurrency(
transaction.potential_supplier_invoice!.total,
transaction.potential_supplier_invoice!.currency,
)}
</span>
</div>
</div>
)}
{/* Amount comparison */}
{(() => {
const txAmount = transaction.amount
const invAmount = transaction.potential_invoice!.total
const sameCurrency = transaction.currency === transaction.potential_invoice!.currency
const amountsMatch = sameCurrency && Math.abs(txAmount - invAmount) < 0.01
const txAbs = Math.abs(transaction.amount)
const invTotal = isSupplierInvoice
? transaction.potential_supplier_invoice!.remaining_amount ?? transaction.potential_supplier_invoice!.total
: transaction.potential_invoice!.total
const invCurrency = isSupplierInvoice
? transaction.potential_supplier_invoice!.currency
: transaction.potential_invoice!.currency
const sameCurrency = transaction.currency === invCurrency
const amountsMatch = sameCurrency && Math.abs(txAbs - invTotal) < 0.01
if (amountsMatch) {
return (
<div className="flex items-center gap-2 p-3 rounded-lg bg-success/10 text-success">
<CheckCircle2 className="h-4 w-4 flex-shrink-0" />
<p className="text-sm font-medium">Beloppen stammer</p>
<p className="text-sm font-medium">Beloppen stämmer</p>
</div>
)
}
const diff = Math.abs(txAmount - invAmount)
const diff = Math.abs(txAbs - invTotal)
return (
<div className="flex items-start gap-2 p-3 rounded-lg bg-warning/10 text-warning-foreground">
<AlertTriangle className="h-4 w-4 flex-shrink-0 mt-0.5" />
@@ -92,6 +133,7 @@ export default function InvoiceMatchDialog({
<p>
Differens: {formatCurrency(diff, transaction.currency)}
{!sameCurrency && ' (olika valutor)'}
{isSupplierInvoice && diff > 0.01 && sameCurrency && ' — fakturan blir delbetald.'}
</p>
</div>
</div>
@@ -102,8 +144,8 @@ export default function InvoiceMatchDialog({
<div className="rounded-lg bg-muted/50 p-4 space-y-2">
<p className="text-sm font-medium">Vid bekräftelse:</p>
<ul className="text-sm text-muted-foreground space-y-1">
<li>• Transaktionen kopplas till fakturan</li>
<li>• Fakturan markeras som betald</li>
<li>• Transaktionen kopplas till {isSupplierInvoice ? 'leverantörsfakturan' : 'fakturan'}</li>
<li>• {isSupplierInvoice ? 'Leverantörsfakturan markeras som betald' : 'Fakturan markeras som betald'}</li>
<li>• Bokföringsverifikation skapas automatiskt</li>
</ul>
</div>
@@ -111,17 +153,10 @@ export default function InvoiceMatchDialog({
)}
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isConfirming}
>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isConfirming}>
Avbryt
</Button>
<Button
onClick={onConfirm}
disabled={isConfirming}
>
<Button onClick={onConfirm} disabled={isConfirming}>
{isConfirming ? 'Bekräftar...' : 'Bekräfta matchning'}
</Button>
</DialogFooter>
+54 -35
View File
@@ -19,86 +19,105 @@ describe('submitFeedback', () => {
vi.stubGlobal('window', {})
}
it('uses Recapt when SDK is present and prepends subject', async () => {
function stubFetchOk() {
const fetchSpy = vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) })
vi.stubGlobal('fetch', fetchSpy)
return fetchSpy
}
it('sends to both Recapt and email when SDK is present', async () => {
const recapt = vi.fn()
stubRecapt(recapt)
const fetchSpy = vi.fn()
vi.stubGlobal('fetch', fetchSpy)
const fetchSpy = stubFetchOk()
const result = await submitFeedback({ subject: 'Hjälpsida', message: 'Hjälp tack' })
expect(result).toEqual({ ok: true, channel: 'recapt' })
expect(result.ok).toBe(true)
expect(result.channels.sort()).toEqual(['email', 'recapt'])
expect(recapt).toHaveBeenCalledWith('feedback', { message: '[Hjälpsida]\n\nHjälp tack' })
expect(fetchSpy).not.toHaveBeenCalled()
expect(fetchSpy).toHaveBeenCalledWith(
'/api/support/contact',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ subject: 'Hjälpsida', message: 'Hjälp tack' }),
})
)
})
it('uses Recapt without subject prefix when subject omitted', async () => {
it('omits subject prefix in Recapt payload when subject not provided', async () => {
const recapt = vi.fn()
stubRecapt(recapt)
stubFetchOk()
await submitFeedback({ message: 'plain' })
expect(recapt).toHaveBeenCalledWith('feedback', { message: 'plain' })
})
it('falls back to email when Recapt throws', async () => {
it('still reports success via email when Recapt throws', async () => {
stubRecapt(() => {
throw new Error('boom')
})
const fetchSpy = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({}),
})
vi.stubGlobal('fetch', fetchSpy)
stubFetchOk()
const result = await submitFeedback({ subject: 'X', message: 'msg' })
expect(result).toEqual({ ok: true, channel: 'email' })
expect(fetchSpy).toHaveBeenCalledWith(
'/api/support/contact',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ subject: 'X', message: 'msg' }),
})
)
expect(result.ok).toBe(true)
expect(result.channels).toEqual(['email'])
})
it('falls back to email when Recapt SDK is absent', async () => {
it('uses email only when Recapt SDK is absent', async () => {
stubNoRecapt()
const fetchSpy = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({}),
})
vi.stubGlobal('fetch', fetchSpy)
const fetchSpy = stubFetchOk()
const result = await submitFeedback({ message: 'msg' })
expect(result).toEqual({ ok: true, channel: 'email' })
expect(result.ok).toBe(true)
expect(result.channels).toEqual(['email'])
expect(fetchSpy).toHaveBeenCalledOnce()
})
it('returns failure with error from email when fetch returns non-ok', async () => {
stubNoRecapt()
const fetchSpy = vi.fn().mockResolvedValue({
ok: false,
json: async () => ({ error: 'Mailtjänsten är inte konfigurerad' }),
it('reports success when Recapt succeeds even if email fails', async () => {
const recapt = vi.fn()
stubRecapt(recapt)
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({ ok: false, json: async () => ({ error: 'down' }) })
)
const result = await submitFeedback({ message: 'msg' })
expect(result.ok).toBe(true)
expect(result.channels).toEqual(['recapt'])
})
it('returns failure with email error when both channels fail', async () => {
stubRecapt(() => {
throw new Error('boom')
})
vi.stubGlobal('fetch', fetchSpy)
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: false,
json: async () => ({ error: 'Mailtjänsten är inte konfigurerad' }),
})
)
const result = await submitFeedback({ message: 'msg' })
expect(result.ok).toBe(false)
expect(result.channel).toBe('email')
expect(result.channels).toEqual([])
expect(result.error).toBe('Mailtjänsten är inte konfigurerad')
})
it('returns failure when fetch itself throws', async () => {
it('returns failure when fetch itself throws and Recapt is absent', async () => {
stubNoRecapt()
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Network down')))
const result = await submitFeedback({ message: 'msg' })
expect(result.ok).toBe(false)
expect(result.channels).toEqual([])
expect(result.error).toBe('Network down')
})
})
+35 -15
View File
@@ -3,9 +3,11 @@ export interface SubmitFeedbackInput {
subject?: string
}
export type SupportChannel = 'recapt' | 'email'
export interface SubmitFeedbackResult {
ok: boolean
channel: 'recapt' | 'email'
channels: SupportChannel[]
error?: string
}
@@ -14,7 +16,9 @@ function composeMessage({ message, subject }: SubmitFeedbackInput): string {
return `[${subject}]\n\n${message}`
}
async function submitViaEmail({ message, subject }: SubmitFeedbackInput): Promise<SubmitFeedbackResult> {
async function submitViaEmail(
{ message, subject }: SubmitFeedbackInput
): Promise<{ ok: true } | { ok: false; error: string }> {
try {
const res = await fetch('/api/support/contact', {
method: 'POST',
@@ -23,26 +27,42 @@ async function submitViaEmail({ message, subject }: SubmitFeedbackInput): Promis
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
return { ok: false, channel: 'email', error: data.error || 'Kunde inte skicka meddelandet' }
return { ok: false, error: data.error || 'Kunde inte skicka meddelandet' }
}
return { ok: true, channel: 'email' }
return { ok: true }
} catch (err) {
return { ok: false, channel: 'email', error: err instanceof Error ? err.message : 'Nätverksfel' }
return { ok: false, error: err instanceof Error ? err.message : 'Nätverksfel' }
}
}
function submitViaRecapt(
input: SubmitFeedbackInput
): { ok: true } | { ok: false; error: string } | null {
const recapt = typeof window !== 'undefined' ? window.recapt : undefined
if (typeof recapt !== 'function') return null
try {
recapt('feedback', { message: composeMessage(input) })
return { ok: true }
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : 'Recapt-fel' }
}
}
export async function submitFeedback(input: SubmitFeedbackInput): Promise<SubmitFeedbackResult> {
const recapt = typeof window !== 'undefined' ? window.recapt : undefined
const fullMessage = composeMessage(input)
const recaptResult = submitViaRecapt(input)
const emailResult = await submitViaEmail(input)
if (typeof recapt === 'function') {
try {
recapt('feedback', { message: fullMessage })
return { ok: true, channel: 'recapt' }
} catch {
// fall through to email
}
const channels: SupportChannel[] = []
if (recaptResult?.ok) channels.push('recapt')
if (emailResult.ok) channels.push('email')
if (channels.length > 0) {
return { ok: true, channels }
}
return submitViaEmail(input)
return {
ok: false,
channels: [],
error: emailResult.ok ? undefined : emailResult.error,
}
}