#!/usr/bin/env python3
"""Build unsealed canonical Codex Security JSON from the enriched ledger."""

from __future__ import annotations

import json
from pathlib import Path


SCAN_ID = "45fc7c79-225a-4c49-a5d0-2d456346a5f5"
REVISION = "a782c32b3a1c823e4f803ca07137187fbd2d32cb"
TARGET_ID = "target_sha256_45649abc0fb1796e90962e6b91b69d3dc30f656eb9aa20d048dd4165f65a078c"
SCAN_DIR = Path(
    "/private/var/folders/kx/8xhl68dn107bzv0r4bc6l7dr0000gn/T/"
    "codex-security-scans-QM6XF1/ai_api/"
    "a782c32b3a1c823e4f803ca07137187fbd2d32cb_20260729T024025Z_zco_cdqv"
)
LEDGER = SCAN_DIR / "artifacts/02_discovery/candidate_ledger.jsonl"

META = {
    "baidu-face-empty-allowlist": {
        "title": "Empty Baidu face allowlist fails open",
        "rule": "access-control.fail-open-allowlist",
        "anchor": "baidu-face-allowlist-empty-allows-all",
        "category": "Access-control fail-open",
        "remediation": "Reject startup or every request when `allowed_ips` is empty; add authenticated device/service credentials and proxy-aware source validation.",
        "tests": ["Assert an empty or missing allowlist returns 403 for every face operation.", "Assert an allowed source still requires a valid service or device credential."],
        "controls": ["Fail-closed configuration validation", "Authenticated internal-service boundary", "Ingress restriction and audit logging"],
    },
    "zhaiker-forged-exam-callback": {
        "title": "Unsigned Zhaiker callback can forge user health records",
        "rule": "callback-authenticity.health-import",
        "anchor": "zhaiker-callback-without-authenticity",
        "category": "Missing callback authentication",
        "remediation": "Require a vendor signature over the raw body plus timestamp and nonce, reject replays, and bind the vendor subject to the expected user before writes.",
        "tests": ["Reject missing, invalid, expired, and replayed callback signatures.", "Reject a valid vendor callback whose subject is not bound to the target user."],
        "controls": ["Signed callback middleware", "Nonce replay store", "Vendor-to-user binding invariant"],
    },
    "tracked-environment-and-sql-credentials": {
        "title": "Repository contains environment and SQL credential material",
        "rule": "hardcoded-credentials.repository-artifacts",
        "anchor": "tracked-runtime-credentials",
        "category": "Hardcoded credentials",
        "remediation": "Rotate every potentially exposed credential, remove values from the current tree and history where authorized, store only templates, and inject secrets through the deployment secret store.",
        "tests": ["Run a secret scanner against the full Git history and fail on non-placeholder values.", "Verify rotated credentials are rejected and new credentials are available only at runtime."],
        "controls": ["Codeup secret scanning", "Pre-commit secret detection", "Central secret rotation and inventory"],
    },
    "bank-bind-user-login-token-mint": {
        "title": "Bank identity binding mints a token without bank-session proof",
        "rule": "authentication.bank-identity-binding",
        "anchor": "bank-binding-without-identity-proof",
        "category": "Authentication bypass",
        "remediation": "Accept the bank identity only from a server-verified authorization code/session, bind all identity fields to that result, and never trust caller-supplied bank identifiers.",
        "tests": ["Reject binding without a verified bank session.", "Reject mismatched bank identity, staff identity, school, expired state, and replay."],
        "controls": ["Central bank-session authentication middleware", "Server-side identity binding", "Single-use authorization state"],
    },
    "bank-user-id-account-switch": {
        "title": "Bank user ID permits linked-account enumeration and token issuance",
        "rule": "authorization.bank-account-switch",
        "anchor": "bank-user-id-not-bound-to-caller",
        "category": "Authorization bypass / IDOR",
        "remediation": "Derive `bank_user_id` from the verified bank session, require the current session to own every relation, and prevent public linked-user enumeration.",
        "tests": ["Reject related-user queries and account switches for a different bank identity.", "Confirm a verified owner can switch only to relations in the same school scope."],
        "controls": ["Object ownership enforcement", "Opaque external identifiers", "Token-mint audit events"],
    },
    "terminal-face-sync-without-device-auth": {
        "title": "Terminal face sync exposes biometric data without device authentication",
        "rule": "device-authentication.face-sync",
        "anchor": "terminal-face-whitelist-without-device-credential",
        "category": "Missing device authentication",
        "remediation": "Remove face synchronization from the anonymous whitelist and require a per-device credential or signed request bound to store, timestamp, nonce, and operation.",
        "tests": ["Reject face sync and feedback without a valid device credential.", "Reject replay, cross-store device IDs, and stale signatures."],
        "controls": ["Per-device credentials", "Request signing and replay protection", "Biometric access audit trail"],
    },
    "terminal-role-authorization-disabled": {
        "title": "Terminal role authorization is disabled",
        "rule": "authorization.terminal-role-check",
        "anchor": "terminal-check-privilege-no-op",
        "category": "Missing function-level authorization",
        "remediation": "Restore fail-closed route authorization using the same permission model as the store application, then explicitly register every terminal API and role.",
        "tests": ["Assert low-privilege users cannot manage administrators, payment settings, or refunds.", "Assert unknown or unregistered terminal routes are denied."],
        "controls": ["Central deny-by-default authorization", "Role-to-route policy tests", "Privileged-action audit logging"],
    },
    "healthkit-token-logging": {
        "title": "HealthKit access and refresh tokens are logged",
        "rule": "secret-exposure.oauth-token-logging",
        "anchor": "healthkit-token-array-logged",
        "category": "Sensitive information in logs",
        "remediation": "Never log token response objects; log only redacted metadata such as provider, user binding result, expiry, and a correlation ID, then rotate exposed tokens where possible.",
        "tests": ["Capture application logs during OAuth exchange and assert no access or refresh token appears.", "Verify structured logger redaction covers nested credential fields."],
        "controls": ["Central logging redaction", "Sensitive-field schema annotations", "Log access and retention limits"],
    },
    "healthkit-oauth-state-unbound": {
        "title": "HealthKit OAuth state is not session-bound or single-use",
        "rule": "oauth.state-binding",
        "anchor": "healthkit-state-syntax-only-validation",
        "category": "OAuth login CSRF / account misbinding",
        "remediation": "Generate high-entropy state server-side, bind it to the initiating application user and redirect, enforce short expiry and single use, and consume it atomically before token exchange.",
        "tests": ["Reject unknown, expired, reused, or other-user state values.", "Complete one authorized callback and verify the same state cannot be consumed again."],
        "controls": ["Server-side OAuth transaction store", "Atomic state consumption", "Provider-account binding audit"],
    },
    "prepay-concurrent-orphan-orders": {
        "title": "Concurrent prepay scans can create duplicate orphan orders",
        "rule": "race-condition.prepay-order",
        "anchor": "prepay-link-check-before-lock",
        "category": "Race condition / missing idempotency",
        "remediation": "Lock the prepay row inside the transaction or atomically claim it before creating the meal order; verify affected rows and roll back the losing request.",
        "tests": ["Issue parallel scans for the same prepay row and assert exactly one meal order is committed.", "Assert the losing request returns the already-linked order or an idempotent conflict."],
        "controls": ["Database uniqueness and row locking", "Idempotency keys", "Affected-row assertions"],
    },
    "refund-order-goods-horizontal-bypass": {
        "title": "Refund request is not bound to the current order owner",
        "rule": "authorization.refund-order-object",
        "anchor": "refund-goods-lookup-without-owner",
        "category": "Authorization bypass / IDOR",
        "remediation": "Resolve `orderGoodsId` through the current authenticated user's order relationship and reject any owner mismatch before disclosure or refund-row creation.",
        "tests": ["Reject reading or refunding another user's same-store order goods.", "Confirm merchant approval cannot override a missing original-owner binding."],
        "controls": ["Owner-scoped repository methods", "Authorization tests for every object ID", "Refund workflow anomaly detection"],
    },
    "anonymous-image-upload": {
        "title": "Image upload disables authentication when flag is omitted",
        "rule": "resource-abuse.anonymous-upload",
        "anchor": "upload-empty-flag-disables-login",
        "category": "Unauthenticated resource consumption",
        "remediation": "Require authentication by default, remove request-controlled authentication toggles, enforce decoded image MIME validation, and apply tenant/user quotas and rate limits.",
        "tests": ["Reject uploads without a valid access token regardless of `flag`.", "Enforce file type, per-user quota, and request-rate limits."],
        "controls": ["Authentication middleware", "Upload quotas and throttling", "Content re-encoding and malware scanning"],
    },
}


def write_json(path: Path, value: object) -> None:
    path.write_text(
        json.dumps(value, ensure_ascii=False, indent=2, sort_keys=False) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    rows = [
        json.loads(line)
        for line in LEDGER.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]
    reportable = [
        row
        for row in rows
        if row["validation"]["disposition"] == "reportable"
        and row["attack_path"]["decision"] == "reportable"
    ]

    findings = []
    for row in reportable:
        instance = row["instance"]
        meta = META[instance]
        locations = []
        for loc in row["locations"]:
            output = {
                "path": loc["path"],
                "startLine": loc["start_line"],
                "role": loc["role"],
            }
            if "end_line" in loc:
                output["endLine"] = loc["end_line"]
            locations.append(output)
        findings.append(
            {
                "ruleId": meta["rule"],
                "identity": {"anchor": meta["anchor"], "instance": instance},
                "title": meta["title"],
                "summary": row["summary"],
                "severity": {
                    "level": row["attack_path"]["severity"],
                    "rationale": row["attack_path"]["severity_rationale"],
                    "changeConditions": row["attack_path"]["change_conditions"],
                },
                "confidence": {
                    "level": row["validation"]["confidence"],
                    "rationale": row["validation"]["confidence_rationale"],
                },
                "taxonomy": {
                    "category": meta["category"],
                    "cwe": row["cwe_ids"],
                },
                "locations": locations,
                "rootCause": {
                    "summary": row["evidence"],
                },
                "remediation": meta["remediation"],
                "validation": {
                    "method": row["validation"]["method"],
                    "summary": row["evidence"],
                    "assertions": row["validation"]["rubric"],
                    "limitations": [row["validation"]["remaining_uncertainty"]],
                },
                "attackPath": {
                    "summary": row["attack_path"]["reachability"],
                    "dataflow": {
                        "summary": row["attack_path"]["dataflow"],
                        "source": row["locations"][0]["path"],
                        "sink": row["locations"][-1]["path"],
                        "outcome": row["summary"],
                    },
                    "reachability": {
                        "summary": row["attack_path"]["reachability"],
                        "attacker": "network caller, repository reader, device caller, or lower-privilege authenticated user as described",
                        "entrypoint": row["locations"][0]["path"],
                        "outcome": row["summary"],
                    },
                    "impact": {
                        "level": row["attack_path"]["impact"],
                        "why": row["summary"],
                    },
                    "likelihood": {
                        "level": row["attack_path"]["likelihood"],
                        "why": row["attack_path"]["reachability"],
                    },
                    "limitations": [row["attack_path"]["counterevidence"]],
                },
                "remediationTests": meta["tests"],
                "preventiveControls": meta["controls"],
                "provenance": {"source": "local_plugin"},
                "extensions": {"candidateId": row["candidate_id"]},
            }
        )

    rank = {"high": 0, "medium": 1, "low": 2}
    findings.sort(key=lambda item: (rank[item["severity"]["level"]], item["title"]))

    manifest = {
        "scan": {
            "target": {
                "kind": "git_revision",
                "targetId": TARGET_ID,
                "displayName": "ai_api",
                "revision": REVISION,
            },
            "scope": {
                "includePaths": ["."],
                "excludePaths": [],
                "summary": "Standard repository scan of the immutable local ai_api Git revision from the authorized Codeup/local workflow. All 6,288 tracked files were inventoried and read; security-relevant first-party surfaces received source/control/sink review.",
                "artifactsReviewed": [
                    "artifacts/01_context/threat_model.md",
                    "artifacts/02_discovery/in_scope_files.txt",
                    "artifacts/02_discovery/work_ledger.jsonl",
                    "artifacts/02_discovery/candidate_ledger.jsonl",
                ],
                "runtimeStatus": "Static validation only; no production endpoint, credential, payment, database mutation, or external callback was exercised.",
                "validationMode": "Large internal repository static source-control-sink trace with explicit deployment proof gaps.",
                "context": "Continuation of the 2026-06-24 Codex Security review. Code remained local/Codeup and was not uploaded to GitHub or modified.",
                "limitations": [
                    "No authorized production ingress, reverse-proxy, database-schema, or live OAuth/callback test was performed.",
                    "Binary and non-UTF8 files were fully byte-read and inventoried but not semantically decoded.",
                    "Dependency advisory resolution was not performed against a current package advisory service.",
                ],
            },
            "threatModel": {
                "summary": "The service processes payments, orders, balances, tenant data, biometric material, health records, device traffic, and third-party callbacks across public API, merchant, terminal, persistence, and vendor trust boundaries.",
                "assets": [
                    "User identities and bearer tokens",
                    "Orders, balances, refunds, settlement and payment state",
                    "Biometric templates and face synchronization data",
                    "Health, examination, and weight records",
                    "Tenant, school, store, merchant, and device ownership",
                    "Runtime and third-party credentials",
                ],
                "trustBoundaries": [
                    "Internet or untrusted client to API routes",
                    "Authenticated user to privileged business action",
                    "Terminal or device to server",
                    "Third-party callback to trusted persistence",
                    "Application to database, cache, queue, storage, and logs",
                    "Operator configuration to runtime enforcement",
                ],
                "attackerCapabilities": [
                    "Send crafted HTTP parameters, headers, JSON, files, and concurrent requests",
                    "Possess a low-privilege user or terminal account",
                    "Know or enumerate business and device identifiers",
                    "Read a repository clone or application logs when granted access",
                ],
                "securityObjectives": [
                    "Authenticate callback, device, and bank identity boundaries",
                    "Enforce tenant, owner, role, and object authorization",
                    "Keep configuration controls fail-closed",
                    "Protect credentials, biometric data, health data, and payment state",
                    "Make payment and import workflows atomic and replay-safe",
                ],
                "assumptions": [
                    "Deployment may add controls not represented in the repository; these are recorded as proof gaps rather than assumed.",
                    "Missing allowlists, credentials, or mappings must not weaken enforcement.",
                    "A valid identifier alone is not sufficient device, bank-user, or object authorization.",
                ],
            },
        }
    }

    findings_doc = {"findings": findings}
    receipts = [
        "artifacts/02_discovery/candidate_ledger.jsonl",
        "artifacts/02_discovery/work_ledger.jsonl",
        "artifacts/02_discovery/inventory_summary.json",
    ]
    coverage = {
        "completeness": "partial",
        "inventoryStrategy": "repository",
        "includePaths": ["."],
        "excludePaths": [],
        "surfaces": [
            {"id": "biometric-face", "label": "Biometric and face APIs", "riskArea": "Authentication and sensitive-data access", "disposition": "reported", "receiptRefs": receipts, "notes": "Fail-open Baidu allowlist and unauthenticated terminal face synchronization survived validation."},
            {"id": "health-callbacks", "label": "Health and vendor callbacks", "riskArea": "Callback authenticity, OAuth, token handling", "disposition": "reported", "receiptRefs": receipts, "notes": "Unsigned Zhaiker import, HealthKit token logging, and weak OAuth state were reported; callback concurrency needs schema proof."},
            {"id": "bank-auth", "label": "Bank identity and token issuance", "riskArea": "Authentication and account ownership", "disposition": "reported", "receiptRefs": receipts, "notes": "Two independently reachable token-mint paths lack caller ownership proof."},
            {"id": "terminal-authz", "label": "Terminal authentication and authorization", "riskArea": "Device authentication and role enforcement", "disposition": "reported", "receiptRefs": receipts, "notes": "Route privilege enforcement is disabled and a biometric sync route bypasses login."},
            {"id": "credentials", "label": "Configuration, repository secrets, and logs", "riskArea": "Credential disclosure", "disposition": "reported", "receiptRefs": receipts, "notes": "Tracked environment/SQL credentials and unredacted HealthKit token logs were reported; no credential values were copied to artifacts."},
            {"id": "payments-orders-refunds", "label": "Payments, orders, and refunds", "riskArea": "Object authorization and atomic state transitions", "disposition": "reported", "receiptRefs": receipts, "notes": "Refund owner binding and prepay scan idempotency failures survived validation."},
            {"id": "uploads-files", "label": "Uploads and file handling", "riskArea": "Unauthenticated resource consumption and unsafe files", "disposition": "reported", "receiptRefs": receipts, "notes": "Anonymous repeatable image upload survived; no source-proven path traversal or code execution survived."},
            {"id": "tenant-client-gates", "label": "Tenant and client gates", "riskArea": "Fail-open routing and shutdown controls", "disposition": "needs_follow_up", "receiptRefs": receipts, "notes": "Unknown-Host tenant fallback and unknown-platform client bypass need runtime/product-intent confirmation."},
            {"id": "injection-execution", "label": "Command, query, deserialization, SSRF, and dynamic execution hotspots", "riskArea": "Injection and unsafe parsing", "disposition": "no_issue_found", "receiptRefs": receipts, "notes": "All tracked files were pattern-inventoried and first-party hits were traced; no additional source-proven reportable instance survived."},
            {"id": "dependencies-vendor", "label": "Vendored dependencies and binary assets", "riskArea": "Third-party and binary risk", "disposition": "no_issue_found", "receiptRefs": receipts, "notes": "Files were byte-read and inventoried. A current external advisory audit was outside this scan and remains a limitation."},
        ],
        "explicitExclusions": [],
        "deferred": [
            {"id": "candidate-zhaiker-race", "reason": "Need deployed uniqueness constraints and a concurrent integration test.", "paths": ["app/api/service/ZhaikerExamImporter.php"], "surfaceIds": ["health-callbacks"]},
            {"id": "candidate-default-tenant-host", "reason": "Need an authorized alternate-Host or direct-IP ingress test.", "paths": ["app/common.php", "app/api/controller/Controller.php"], "surfaceIds": ["tenant-client-gates"]},
            {"id": "candidate-client-disable-gate", "reason": "Need product confirmation that the enable switch must cover unknown clients.", "paths": ["app/api/controller/Controller.php", "app/api/common.php"], "surfaceIds": ["tenant-client-gates"]},
        ],
        "openQuestions": [
            {"question": "Does production reject or authenticate requests to bank, Baidu face, Zhaiker callback, and terminal face routes at the gateway?", "followUpPrompt": f"Against authorized staging for {REVISION}, verify gateway authentication and ingress policy for the named routes without changing production data."},
            {"question": "Are the tracked environment and SQL credentials active?", "followUpPrompt": "Have the credential owners rotate and inventory the referenced secrets without posting their values into Codex or logs."},
            {"question": "Does the production Zhaiker session table enforce a unique vendor/raw_hash constraint?", "followUpPrompt": "Run an authorized read-only SHOW CREATE TABLE and compare the result with the Zhaiker importer race candidate."},
            {"question": "Can an unmapped Host reach this application and fall back to tenant 10001?", "followUpPrompt": "Run an authorized staging request with an unmapped Host and confirm the request is rejected before tenant resolution."},
        ],
    }

    write_json(SCAN_DIR / "scan-manifest.json", manifest)
    write_json(SCAN_DIR / "findings.json", findings_doc)
    write_json(SCAN_DIR / "coverage.json", coverage)


if __name__ == "__main__":
    main()
