{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/security-finding.schema.json",
  "version": "1.0.0",
  "title": "Multi-Agent Pipeline  -  security finding",
  "description": "Contract for one finding produced by the security-auditor subagent (Phase 3) and the /multi-agent:security-review command. A security finding IS a reviewer-output.schema.json finding - same core fields (severity, file, line, issue, fix) - carrying one extra `security` block. That is exactly why it works: the auditor emits reviewer-output objects whose findings have the block, they merge into the Phase 3 reviewer set at Step 3.0, and a blocking one blocks Phase 4 like any reviewer blocker. reviewer-output.schema.json declares the same `security` property as OPTIONAL on its finding; here it is REQUIRED, which is the whole difference between a general finding and a security finding. The severity enum is the reviewer's (blocking|important|suggestion), never a private Critical/High/Medium scale: severity is derived from security.cvss.band by the rule on the `severity` property, and a severity that contradicts the band is the one inconsistency this contract forbids. The two schemas are kept in step by smoke-security-schema-parity.sh, not by a cross-file $ref, matching the self-contained style of the other pipeline schemas.",
  "type": "object",
  "additionalProperties": false,
  "required": ["severity", "file", "line", "issue", "fix", "security"],
  "properties": {
    "severity": {
      "type": "string",
      "enum": ["blocking", "important", "suggestion"],
      "description": "The reviewer severity, derived from security.cvss.band so this finding filters through triage like any other: critical or high band -> blocking; medium -> important; low or none -> suggestion. It is not a free choice - a severity that disagrees with the band is the one inconsistency this schema exists to forbid."
    },
    "file": {
      "type": "string",
      "minLength": 1,
      "description": "Path relative to repo root. The auditor must not invent a file that is not in the tree."
    },
    "line": {
      "type": "integer",
      "minimum": 0,
      "description": "Line number. 0 = whole-file, configuration-level, or dependency-manifest finding."
    },
    "issue": {
      "type": "string",
      "minLength": 4,
      "description": "What is wrong, in one sentence. The vulnerability, not the fix."
    },
    "fix": {
      "type": "string",
      "minLength": 4,
      "description": "Concrete remediation in one line, for the reviewer-shaped view. The full before/after diff lives in security.remediationDiff."
    },
    "ruleId": {
      "type": "string",
      "minLength": 1,
      "description": "Stable id of a cited rule (e.g. SEC-03) when the finding comes from a standards registry supplied to the auditor. The author can look the rule up and argue with it rather than with the auditor."
    },
    "criteriaSource": {
      "type": "string",
      "minLength": 1,
      "description": "Which source the rule came from: a registry name, a compliance catalog (apple-archive-compliance / google-play-compliance), a reference path, or 'threat-model' when the finding is derived from the run-scoped threat model rather than a fixed rule."
    },
    "security": {
      "type": "object",
      "additionalProperties": false,
      "required": ["owaspCategory", "cwe", "cvss", "evidence", "confidence", "remediation"],
      "description": "The security envelope. Present on every security finding; optional on a general reviewer finding.",
      "properties": {
        "owaspCategory": {
          "type": "string",
          "pattern": "^(A(0[1-9]|10):2021|M[1-9]:2024|M10:2024)( .+)?$",
          "description": "The OWASP category: web/API uses OWASP Top 10 2021 ids (A01:2021 .. A10:2021), mobile uses OWASP Mobile Top 10 2024 ids (M1:2024 .. M10:2024). An optional human title may follow the id, e.g. 'A01:2021 Broken Access Control'."
        },
        "cwe": {
          "type": "string",
          "pattern": "^CWE-[0-9]{1,5}$",
          "description": "The specific weakness, as a validated CWE id (CWE-89, CWE-798, ...). One weakness per finding; if a line has two, it is two findings."
        },
        "cve": {
          "type": "string",
          "pattern": "^CVE-[0-9]{4}-[0-9]{4,}$",
          "description": "A published CVE, when the finding is a known-vulnerable dependency rather than first-party code. Set by the dependency-audit path, not by static code review."
        },
        "cvss": {
          "type": "object",
          "additionalProperties": false,
          "required": ["vector", "baseScore", "band"],
          "description": "CVSS 3.1 base metrics. The auditor writes the vector; security_cvss_score (toolkit) computes baseScore and band from it, so the two cannot drift from the vector by hand.",
          "properties": {
            "vector": {
              "type": "string",
              "pattern": "^CVSS:3[.]1/AV:[NALP]/AC:[LH]/PR:[NLH]/UI:[NR]/S:[UC]/C:[NLH]/I:[NLH]/A:[NLH]$",
              "description": "A full CVSS 3.1 base vector string. All eight base metrics are required; temporal and environmental metrics are out of scope for a static review."
            },
            "baseScore": {
              "type": "number",
              "minimum": 0,
              "maximum": 10,
              "description": "The CVSS 3.1 base score computed from the vector by security_cvss_score. 0.0 .. 10.0, one decimal."
            },
            "band": {
              "type": "string",
              "enum": ["none", "low", "medium", "high", "critical"],
              "description": "The qualitative band of baseScore: none 0.0, low 0.1-3.9, medium 4.0-6.9, high 7.0-8.9, critical 9.0-10.0. This is what maps to `severity`."
            }
          }
        },
        "evidence": {
          "type": "string",
          "minLength": 8,
          "description": "What in the code proves the finding, quoted or cited by file:line. A static finding with no evidence is a guess; this is what a reader checks before agreeing."
        },
        "counterevidence": {
          "type": "string",
          "description": "What in the code would DISPROVE the finding, or the condition under which it is a false positive - a guard elsewhere, a framework default, an unreachable path. Stating it keeps confidence honest; leaving it blank asserts there is none."
        },
        "confidence": {
          "type": "string",
          "enum": ["high", "medium", "low"],
          "description": "How sure the auditor is that this is real. Low confidence does not mean do-not-report; it means report with the counterevidence and let triage weigh it."
        },
        "confidenceRationale": {
          "type": "string",
          "description": "One line on why the confidence is what it is - what was and was not verifiable from the code alone."
        },
        "severityChangeConditions": {
          "type": "string",
          "description": "The fact that, if learned, would move the severity: 'critical if this endpoint is unauthenticated in production; medium if it is admin-only'. Names the assumption the score rests on."
        },
        "remediation": {
          "type": "string",
          "minLength": 8,
          "description": "The remediation steps in prose - what to change and why it closes the weakness. Actionable, not 'sanitize input'."
        },
        "remediationDiff": {
          "type": "object",
          "additionalProperties": false,
          "required": ["before", "after"],
          "description": "The fix as a before/after pair the author can read as a diff. Optional - some findings are configuration or process changes with no single code hunk - but preferred for first-party code.",
          "properties": {
            "before": { "type": "string", "description": "The vulnerable code as it stands." },
            "after": { "type": "string", "description": "The same region after the fix." }
          }
        },
        "endpoint": {
          "type": "string",
          "description": "The HTTP route or RPC method the finding sits on, when it is a service-layer issue. Empty for non-network findings."
        },
        "method": {
          "type": "string",
          "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
          "description": "The HTTP method for an endpoint finding."
        },
        "fixVerification": {
          "type": "string",
          "description": "How to confirm the fix worked - the test to add or the check to run. Static review cannot fire an exploit, so this is the empirical step a human or a later dynamic pass takes."
        }
      }
    }
  }
}
