diff --git a/components/security-map/securityMap.generated.ts b/components/security-map/securityMap.generated.ts index 201b7d9e..29c6c8f9 100644 --- a/components/security-map/securityMap.generated.ts +++ b/components/security-map/securityMap.generated.ts @@ -117,6 +117,30 @@ export const securityMapGraph = { "normal-operations" ] }, + { + "id": "asset-seed-secrets", + "type": "asset", + "title": "Seed phrase and recovery secrets", + "summary": "The mnemonic or recovery material that reconstructs signing keys. Anyone who sees it can take the wallet.", + "domains": [ + "devices-identity", + "people" + ], + "status": "proposed", + "tags": [ + "seed-phrase", + "recovery", + "keys" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations", + "incident-response" + ], + "framework": "wallet-security" + }, { "id": "asset-signer-keys", "type": "asset", @@ -395,6 +419,28 @@ export const securityMapGraph = { ], "framework": "governance" }, + { + "id": "component-hardware-wallet", + "type": "component", + "title": "Hardware wallet", + "summary": "A dedicated signing device that keeps keys offline and shows destination and amount on its own screen.", + "domains": [ + "devices-identity", + "governance-treasury" + ], + "status": "proposed", + "tags": [ + "hardware-wallet", + "cold-wallet" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations" + ], + "framework": "wallet-security" + }, { "id": "component-hiring-pipeline", "type": "component", @@ -506,6 +552,29 @@ export const securityMapGraph = { "normal-operations" ] }, + { + "id": "component-seed-backup", + "type": "component", + "title": "Seed backup", + "summary": "Offline copies of the seed phrase: paper, metal, or split shares. Physical access here is wallet takeover.", + "domains": [ + "devices-identity", + "people" + ], + "status": "proposed", + "tags": [ + "seed-phrase", + "backup" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations", + "recovery" + ], + "framework": "wallet-security" + }, { "id": "component-signers", "type": "component", @@ -550,6 +619,28 @@ export const securityMapGraph = { ], "framework": "external-security-reviews" }, + { + "id": "component-software-wallet", + "type": "component", + "title": "Software wallet", + "summary": "A hot wallet on a phone or browser that signs from an internet-connected device. Convenience with a larger remote attack surface.", + "domains": [ + "devices-identity", + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "software-wallet", + "hot-wallet" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations" + ], + "framework": "wallet-security" + }, { "id": "control-branch-protection", "type": "control", @@ -596,6 +687,30 @@ export const securityMapGraph = { "assessmentEligible": true, "framework": "infrastructure" }, + { + "id": "control-cold-wallet-separation", + "type": "control", + "title": "Cold and hot wallet separation", + "summary": "Keep high-value keys on offline or hardware signers. Use a hot wallet only for amounts you can afford to lose.", + "domains": [ + "devices-identity", + "governance-treasury" + ], + "status": "proposed", + "tags": [ + "cold-wallet", + "isolation" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations" + ], + "controlClass": "preventive", + "assessmentEligible": true, + "framework": "wallet-security" + }, { "id": "control-dependency-pinning-provenance", "type": "control", @@ -887,6 +1002,30 @@ export const securityMapGraph = { "assessmentEligible": true, "framework": "iam" }, + { + "id": "control-limited-token-approvals", + "type": "control", + "title": "Limited token approvals", + "summary": "Approve only the amount needed for this transaction. Revoke leftover allowances. Avoid permit and infinite approve by default.", + "domains": [ + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "approvals", + "allowance" + ], + "roles": [ + "engineer-developer", + "multisig-signer" + ], + "lifecycle": [ + "normal-operations" + ], + "controlClass": "preventive", + "assessmentEligible": true, + "framework": "wallet-security" + }, { "id": "control-multisig-threshold-policy", "type": "control", @@ -936,6 +1075,31 @@ export const securityMapGraph = { "assessmentEligible": true, "framework": "infrastructure" }, + { + "id": "control-offline-seed-custody", + "type": "control", + "title": "Offline seed custody", + "summary": "Keep the seed offline, preferably durable and split. Treat any screen, photo, or cloud copy as compromise and rotate.", + "domains": [ + "devices-identity", + "people" + ], + "status": "proposed", + "tags": [ + "seed-phrase", + "offline" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations", + "recovery" + ], + "controlClass": "preventive", + "assessmentEligible": true, + "framework": "wallet-security" + }, { "id": "control-onchain-anomaly-detection", "type": "control", @@ -1131,6 +1295,30 @@ export const securityMapGraph = { "assessmentEligible": true, "framework": "multisig-for-protocols" }, + { + "id": "control-simulate-before-sign", + "type": "control", + "title": "Simulate and decode before signing", + "summary": "Independently simulate the transaction and read destination, asset, and calldata on a trusted display. Do not sign because a website said it was safe.", + "domains": [ + "devices-identity", + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "simulation", + "verification" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations" + ], + "controlClass": "preventive", + "assessmentEligible": true, + "framework": "wallet-security" + }, { "id": "control-tested-incident-runbooks", "type": "control", @@ -1215,6 +1403,22 @@ export const securityMapGraph = { "href": "/devsecops/code-signing", "framework": "devsecops" }, + { + "id": "guidance-cold-vs-hot", + "type": "guidance", + "title": "Cold vs hot wallets", + "summary": "Match fund tier to connectivity. Cold and hardware signers cut remote theft; hot wallets stay for operational amounts.", + "domains": [ + "devices-identity" + ], + "status": "proposed", + "tags": [ + "cold-wallet", + "hot-wallet" + ], + "href": "/wallet-security/cold-vs-hot-wallet", + "framework": "wallet-security" + }, { "id": "guidance-community", "type": "guidance", @@ -1737,6 +1941,55 @@ export const securityMapGraph = { "href": "/user-team-security/security-training", "framework": "user-team-security" }, + { + "id": "guidance-seed-phrase-management", + "type": "guidance", + "title": "Seed phrase management", + "summary": "Offline backup, metal and split-share options, and the rule that suspected exposure means rotate immediately.", + "domains": [ + "devices-identity", + "people" + ], + "status": "proposed", + "tags": [ + "seed-phrase" + ], + "href": "/wallet-security/seed-phrase-management", + "framework": "wallet-security" + }, + { + "id": "guidance-signing-verification", + "type": "guidance", + "title": "Signing and verification", + "summary": "Never sign blindly. Prefer the hardware screen and independently verified calldata over any web UI claim.", + "domains": [ + "devices-identity", + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "signing", + "verification" + ], + "href": "/wallet-security/signing-and-verification/signing-verification", + "framework": "wallet-security" + }, + { + "id": "guidance-smart-contract-interaction", + "type": "guidance", + "title": "Smart contract interaction security", + "summary": "Verify the contract, simulate, and limit approvals before interacting. Most losses here are interaction mistakes, not stolen keys.", + "domains": [ + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "approvals", + "simulation" + ], + "href": "/wallet-security/smart-contract-interaction-security", + "framework": "wallet-security" + }, { "id": "guidance-supply-chain-overview", "type": "guidance", @@ -1817,6 +2070,22 @@ export const securityMapGraph = { "href": "/opsec/travel/guide", "framework": "opsec" }, + { + "id": "guidance-wallet-security", + "type": "guidance", + "title": "Wallet Security", + "summary": "Custody models, hot versus cold tradeoffs, signing verification, seed custody, and safer contract interaction.", + "domains": [ + "devices-identity", + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "wallet" + ], + "href": "/wallet-security/overview", + "framework": "wallet-security" + }, { "id": "guidance-zero-trust", "type": "guidance", @@ -2247,6 +2516,29 @@ export const securityMapGraph = { "normal-operations" ] }, + { + "id": "surface-seed-custody", + "type": "attack-surface", + "title": "Seed custody", + "summary": "How the seed is written, stored, split, and retrieved. Digital copies and casual physical access both count as exposure.", + "domains": [ + "devices-identity", + "people" + ], + "status": "proposed", + "tags": [ + "seed-phrase", + "custody" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations", + "recovery" + ], + "framework": "wallet-security" + }, { "id": "surface-signer-lifecycle", "type": "attack-surface", @@ -2268,6 +2560,70 @@ export const securityMapGraph = { "normal-operations" ] }, + { + "id": "surface-token-approvals", + "type": "attack-surface", + "title": "Token approvals", + "summary": "ERC-20 approve and permit grants that let a spender move tokens later. Unlimited allowance outlives the original swap.", + "domains": [ + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "approvals", + "permit", + "erc20" + ], + "roles": [ + "engineer-developer", + "multisig-signer" + ], + "lifecycle": [ + "normal-operations" + ], + "framework": "wallet-security" + }, + { + "id": "surface-transaction-signing", + "type": "attack-surface", + "title": "Transaction signing", + "summary": "The moment a wallet approves calldata. A hostile UI or unread payload turns a good key into an authorized drain.", + "domains": [ + "devices-identity", + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "signing", + "calldata" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations" + ], + "framework": "wallet-security" + }, + { + "id": "threat-blind-signing", + "type": "threat", + "title": "Blind signing", + "summary": "The signer approves attacker-controlled calldata because a website, chat, or wallet UI was trusted instead of the raw transaction.", + "domains": [ + "devices-identity", + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "signing", + "phishing", + "calldata" + ], + "severity": "critical", + "severityBasis": "Default triage is critical because a valid signature spends funds without stealing the key. Impact scales with what the wallet can move.", + "framework": "wallet-security" + }, { "id": "threat-dprk-it-worker-infiltration", "type": "threat", @@ -2458,6 +2814,24 @@ export const securityMapGraph = { ], "framework": "opsec" }, + { + "id": "threat-seed-phrase-compromise", + "type": "threat", + "title": "Seed phrase compromise", + "summary": "The seed is photographed, typed into a connected device, stored in cloud notes, or read by someone with physical access.", + "domains": [ + "devices-identity", + "people" + ], + "status": "proposed", + "tags": [ + "seed-phrase", + "theft" + ], + "severity": "critical", + "severityBasis": "Default triage is critical because the seed reconstructs every derived key. Treat any suspected exposure as full compromise.", + "framework": "wallet-security" + }, { "id": "threat-smart-contract-exploits", "type": "threat", @@ -2521,6 +2895,23 @@ export const securityMapGraph = { "supply-chain-attacks" ], "framework": "supply-chain" + }, + { + "id": "threat-unlimited-token-approval", + "type": "threat", + "title": "Unlimited token approval", + "summary": "A spender keeps uint256 allowance or a permit grant after the intended swap. A later exploit or hostile spender drains the wallet.", + "domains": [ + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "approvals", + "permit" + ], + "severity": "high", + "severityBasis": "Default triage is high because the drain does not need the key, but it is limited to the approved token and spender. Treat as critical when the spender is unaudited or the allowance is protocol-wide.", + "framework": "wallet-security" } ], "edges": [ @@ -2596,6 +2987,13 @@ export const securityMapGraph = { "type": "depends-on", "status": "proposed" }, + { + "id": "depends-on:component-multisig:component-hardware-wallet", + "source": "component-multisig", + "target": "component-hardware-wallet", + "type": "depends-on", + "status": "proposed" + }, { "id": "depends-on:component-multisig:component-signers", "source": "component-multisig", @@ -2603,6 +3001,13 @@ export const securityMapGraph = { "type": "depends-on", "status": "proposed" }, + { + "id": "depends-on:component-signers:component-hardware-wallet", + "source": "component-signers", + "target": "component-hardware-wallet", + "type": "depends-on", + "status": "proposed" + }, { "id": "depends-on:component-smart-contracts:component-multisig", "source": "component-smart-contracts", @@ -2611,6 +3016,13 @@ export const securityMapGraph = { "rationale": "Upgradeable contracts typically grant admin to a protocol multisig.", "status": "proposed" }, + { + "id": "depends-on:component-software-wallet:component-frontend", + "source": "component-software-wallet", + "target": "component-frontend", + "type": "depends-on", + "status": "proposed" + }, { "id": "detects:control-dns-change-monitoring:threat-frontend-dns-hijacking", "source": "control-dns-change-monitoring", @@ -2646,6 +3058,27 @@ export const securityMapGraph = { "type": "detects", "status": "proposed" }, + { + "id": "documented-by:asset-seed-secrets:guidance-seed-phrase-management", + "source": "asset-seed-secrets", + "target": "guidance-seed-phrase-management", + "type": "documented-by", + "status": "proposed" + }, + { + "id": "documented-by:component-hardware-wallet:guidance-cold-vs-hot", + "source": "component-hardware-wallet", + "target": "guidance-cold-vs-hot", + "type": "documented-by", + "status": "proposed" + }, + { + "id": "documented-by:component-software-wallet:guidance-wallet-security", + "source": "component-software-wallet", + "target": "guidance-wallet-security", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:control-branch-protection:guidance-devsecops", "source": "control-branch-protection", @@ -2660,6 +3093,13 @@ export const securityMapGraph = { "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:control-cold-wallet-separation:guidance-cold-vs-hot", + "source": "control-cold-wallet-separation", + "target": "guidance-cold-vs-hot", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:control-dependency-pinning-provenance:guidance-supply-chain-overview", "source": "control-dependency-pinning-provenance", @@ -2744,6 +3184,13 @@ export const securityMapGraph = { "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:control-independent-tx-verification:guidance-signing-verification", + "source": "control-independent-tx-verification", + "target": "guidance-signing-verification", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:control-insider-threat-mitigation:guidance-insider-threat", "source": "control-insider-threat-mitigation", @@ -2772,6 +3219,13 @@ export const securityMapGraph = { "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:control-limited-token-approvals:guidance-smart-contract-interaction", + "source": "control-limited-token-approvals", + "target": "guidance-smart-contract-interaction", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:control-multisig-threshold-policy:guidance-multisig-overview", "source": "control-multisig-threshold-policy", @@ -2793,6 +3247,13 @@ export const securityMapGraph = { "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:control-offline-seed-custody:guidance-seed-phrase-management", + "source": "control-offline-seed-custody", + "target": "guidance-seed-phrase-management", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:control-onchain-anomaly-detection:guidance-threat-detection", "source": "control-onchain-anomaly-detection", @@ -2856,6 +3317,13 @@ export const securityMapGraph = { "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:control-signer-isolation:guidance-cold-vs-hot", + "source": "control-signer-isolation", + "target": "guidance-cold-vs-hot", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:control-signer-isolation:guidance-opsec", "source": "control-signer-isolation", @@ -2870,6 +3338,13 @@ export const securityMapGraph = { "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:control-simulate-before-sign:guidance-signing-verification", + "source": "control-simulate-before-sign", + "target": "guidance-signing-verification", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:control-tested-incident-runbooks:guidance-incident-playbooks", "source": "control-tested-incident-runbooks", @@ -2926,6 +3401,13 @@ export const securityMapGraph = { "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:threat-blind-signing:guidance-signing-verification", + "source": "threat-blind-signing", + "target": "guidance-signing-verification", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:threat-dprk-it-worker-infiltration:guidance-dprk-playbook", "source": "threat-dprk-it-worker-infiltration", @@ -2968,6 +3450,13 @@ export const securityMapGraph = { "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:threat-seed-phrase-compromise:guidance-seed-phrase-management", + "source": "threat-seed-phrase-compromise", + "target": "guidance-seed-phrase-management", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:threat-smart-contract-exploits:guidance-external-security-reviews", "source": "threat-smart-contract-exploits", @@ -2989,6 +3478,13 @@ export const securityMapGraph = { "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:threat-unlimited-token-approval:guidance-smart-contract-interaction", + "source": "threat-unlimited-token-approval", + "target": "guidance-smart-contract-interaction", + "type": "documented-by", + "status": "proposed" + }, { "id": "exposes:component-ci-cd:surface-ci-cd-secrets", "source": "component-ci-cd", @@ -3045,6 +3541,13 @@ export const securityMapGraph = { "type": "exposes", "status": "proposed" }, + { + "id": "exposes:component-hardware-wallet:surface-transaction-signing", + "source": "component-hardware-wallet", + "target": "surface-transaction-signing", + "type": "exposes", + "status": "proposed" + }, { "id": "exposes:component-hiring-pipeline:surface-hiring-contractor-access", "source": "component-hiring-pipeline", @@ -3073,6 +3576,13 @@ export const securityMapGraph = { "type": "exposes", "status": "proposed" }, + { + "id": "exposes:component-seed-backup:surface-seed-custody", + "source": "component-seed-backup", + "target": "surface-seed-custody", + "type": "exposes", + "status": "proposed" + }, { "id": "exposes:component-signers:surface-physical-presence", "source": "component-signers", @@ -3101,6 +3611,20 @@ export const securityMapGraph = { "type": "exposes", "status": "proposed" }, + { + "id": "exposes:component-software-wallet:surface-token-approvals", + "source": "component-software-wallet", + "target": "surface-token-approvals", + "type": "exposes", + "status": "proposed" + }, + { + "id": "exposes:component-software-wallet:surface-transaction-signing", + "source": "component-software-wallet", + "target": "surface-transaction-signing", + "type": "exposes", + "status": "proposed" + }, { "id": "mitigates:control-branch-protection:threat-supply-chain-compromise", "source": "control-branch-protection", @@ -3115,6 +3639,13 @@ export const securityMapGraph = { "type": "mitigates", "status": "proposed" }, + { + "id": "mitigates:control-cold-wallet-separation:threat-blind-signing", + "source": "control-cold-wallet-separation", + "target": "threat-blind-signing", + "type": "mitigates", + "status": "proposed" + }, { "id": "mitigates:control-dependency-pinning-provenance:threat-supply-chain-compromise", "source": "control-dependency-pinning-provenance", @@ -3164,6 +3695,13 @@ export const securityMapGraph = { "type": "mitigates", "status": "proposed" }, + { + "id": "mitigates:control-independent-tx-verification:threat-blind-signing", + "source": "control-independent-tx-verification", + "target": "threat-blind-signing", + "type": "mitigates", + "status": "proposed" + }, { "id": "mitigates:control-independent-tx-verification:threat-governance-manipulation", "source": "control-independent-tx-verification", @@ -3213,6 +3751,13 @@ export const securityMapGraph = { "type": "mitigates", "status": "proposed" }, + { + "id": "mitigates:control-limited-token-approvals:threat-unlimited-token-approval", + "source": "control-limited-token-approvals", + "target": "threat-unlimited-token-approval", + "type": "mitigates", + "status": "proposed" + }, { "id": "mitigates:control-multisig-threshold-policy:threat-duress-coercion", "source": "control-multisig-threshold-policy", @@ -3234,6 +3779,20 @@ export const securityMapGraph = { "type": "mitigates", "status": "proposed" }, + { + "id": "mitigates:control-offline-seed-custody:threat-duress-coercion", + "source": "control-offline-seed-custody", + "target": "threat-duress-coercion", + "type": "mitigates", + "status": "proposed" + }, + { + "id": "mitigates:control-offline-seed-custody:threat-seed-phrase-compromise", + "source": "control-offline-seed-custody", + "target": "threat-seed-phrase-compromise", + "type": "mitigates", + "status": "proposed" + }, { "id": "mitigates:control-phishing-resistant-mfa:threat-leadership-phishing", "source": "control-phishing-resistant-mfa", @@ -3283,6 +3842,13 @@ export const securityMapGraph = { "type": "mitigates", "status": "proposed" }, + { + "id": "mitigates:control-signer-isolation:threat-blind-signing", + "source": "control-signer-isolation", + "target": "threat-blind-signing", + "type": "mitigates", + "status": "proposed" + }, { "id": "mitigates:control-signer-isolation:threat-leadership-phishing", "source": "control-signer-isolation", @@ -3304,6 +3870,13 @@ export const securityMapGraph = { "type": "mitigates", "status": "proposed" }, + { + "id": "mitigates:control-simulate-before-sign:threat-blind-signing", + "source": "control-simulate-before-sign", + "target": "threat-blind-signing", + "type": "mitigates", + "status": "proposed" + }, { "id": "mitigates:control-tested-incident-runbooks:threat-monitoring-gaps", "source": "control-tested-incident-runbooks", @@ -3325,6 +3898,20 @@ export const securityMapGraph = { "type": "protects", "status": "proposed" }, + { + "id": "protects:control-cold-wallet-separation:asset-signer-keys", + "source": "control-cold-wallet-separation", + "target": "asset-signer-keys", + "type": "protects", + "status": "proposed" + }, + { + "id": "protects:control-cold-wallet-separation:component-hardware-wallet", + "source": "control-cold-wallet-separation", + "target": "component-hardware-wallet", + "type": "protects", + "status": "proposed" + }, { "id": "protects:control-external-security-review:component-smart-contracts", "source": "control-external-security-review", @@ -3346,6 +3933,13 @@ export const securityMapGraph = { "type": "protects", "status": "proposed" }, + { + "id": "protects:control-independent-tx-verification:surface-transaction-signing", + "source": "control-independent-tx-verification", + "target": "surface-transaction-signing", + "type": "protects", + "status": "proposed" + }, { "id": "protects:control-isolated-reproducible-builds:asset-deployment-authority", "source": "control-isolated-reproducible-builds", @@ -3353,6 +3947,13 @@ export const securityMapGraph = { "type": "protects", "status": "proposed" }, + { + "id": "protects:control-limited-token-approvals:surface-token-approvals", + "source": "control-limited-token-approvals", + "target": "surface-token-approvals", + "type": "protects", + "status": "proposed" + }, { "id": "protects:control-multisig-threshold-policy:asset-treasury-funds", "source": "control-multisig-threshold-policy", @@ -3360,6 +3961,20 @@ export const securityMapGraph = { "type": "protects", "status": "proposed" }, + { + "id": "protects:control-offline-seed-custody:asset-seed-secrets", + "source": "control-offline-seed-custody", + "target": "asset-seed-secrets", + "type": "protects", + "status": "proposed" + }, + { + "id": "protects:control-offline-seed-custody:surface-seed-custody", + "source": "control-offline-seed-custody", + "target": "surface-seed-custody", + "type": "protects", + "status": "proposed" + }, { "id": "protects:control-phishing-resistant-mfa:component-identity-accounts", "source": "control-phishing-resistant-mfa", @@ -3381,6 +3996,20 @@ export const securityMapGraph = { "type": "protects", "status": "proposed" }, + { + "id": "protects:control-signer-isolation:component-hardware-wallet", + "source": "control-signer-isolation", + "target": "component-hardware-wallet", + "type": "protects", + "status": "proposed" + }, + { + "id": "protects:control-simulate-before-sign:surface-transaction-signing", + "source": "control-simulate-before-sign", + "target": "surface-transaction-signing", + "type": "protects", + "status": "proposed" + }, { "id": "responds-to:response-dns-hijack:threat-frontend-dns-hijacking", "source": "response-dns-hijack", @@ -3430,6 +4059,34 @@ export const securityMapGraph = { "type": "responds-to", "status": "proposed" }, + { + "id": "targets:threat-blind-signing:asset-signer-keys", + "source": "threat-blind-signing", + "target": "asset-signer-keys", + "type": "targets", + "status": "proposed" + }, + { + "id": "targets:threat-blind-signing:asset-treasury-funds", + "source": "threat-blind-signing", + "target": "asset-treasury-funds", + "type": "targets", + "status": "proposed" + }, + { + "id": "targets:threat-blind-signing:asset-user-funds", + "source": "threat-blind-signing", + "target": "asset-user-funds", + "type": "targets", + "status": "proposed" + }, + { + "id": "targets:threat-blind-signing:surface-transaction-signing", + "source": "threat-blind-signing", + "target": "surface-transaction-signing", + "type": "targets", + "status": "proposed" + }, { "id": "targets:threat-dprk-it-worker-infiltration:asset-secrets-credentials", "source": "threat-dprk-it-worker-infiltration", @@ -3451,6 +4108,13 @@ export const securityMapGraph = { "type": "targets", "status": "proposed" }, + { + "id": "targets:threat-duress-coercion:asset-seed-secrets", + "source": "threat-duress-coercion", + "target": "asset-seed-secrets", + "type": "targets", + "status": "proposed" + }, { "id": "targets:threat-duress-coercion:asset-signer-keys", "source": "threat-duress-coercion", @@ -3465,6 +4129,13 @@ export const securityMapGraph = { "type": "targets", "status": "proposed" }, + { + "id": "targets:threat-duress-coercion:surface-seed-custody", + "source": "threat-duress-coercion", + "target": "surface-seed-custody", + "type": "targets", + "status": "proposed" + }, { "id": "targets:threat-frontend-dns-hijacking:asset-domain-control", "source": "threat-frontend-dns-hijacking", @@ -3605,6 +4276,27 @@ export const securityMapGraph = { "type": "targets", "status": "proposed" }, + { + "id": "targets:threat-seed-phrase-compromise:asset-seed-secrets", + "source": "threat-seed-phrase-compromise", + "target": "asset-seed-secrets", + "type": "targets", + "status": "proposed" + }, + { + "id": "targets:threat-seed-phrase-compromise:asset-signer-keys", + "source": "threat-seed-phrase-compromise", + "target": "asset-signer-keys", + "type": "targets", + "status": "proposed" + }, + { + "id": "targets:threat-seed-phrase-compromise:surface-seed-custody", + "source": "threat-seed-phrase-compromise", + "target": "surface-seed-custody", + "type": "targets", + "status": "proposed" + }, { "id": "targets:threat-smart-contract-exploits:asset-treasury-funds", "source": "threat-smart-contract-exploits", @@ -3681,6 +4373,20 @@ export const securityMapGraph = { "target": "surface-package-install", "type": "targets", "status": "proposed" + }, + { + "id": "targets:threat-unlimited-token-approval:asset-user-funds", + "source": "threat-unlimited-token-approval", + "target": "asset-user-funds", + "type": "targets", + "status": "proposed" + }, + { + "id": "targets:threat-unlimited-token-approval:surface-token-approvals", + "source": "threat-unlimited-token-approval", + "target": "surface-token-approvals", + "type": "targets", + "status": "proposed" } ], "taxonomies": { diff --git a/docs/data/security-map/README.md b/docs/data/security-map/README.md index c7edd8e8..75fb8c9f 100644 --- a/docs/data/security-map/README.md +++ b/docs/data/security-map/README.md @@ -26,12 +26,19 @@ Do not hand-edit `public/security-map.json` or `components/security-map/security ```bash pnpm run generate-security-map pnpm run validate:security-map +pnpm run coverage:security-map pnpm run test:security-map pnpm run docs:build ``` `docs:dev` and `docs:build` both generate the graph before Vocs runs. An invalid graph fails the build. +`coverage:security-map` does not fail the build. It prints the work queue: threats with no +control, controls with no guidance, frameworks with zero nodes, and `## Related frameworks` +links whose two framework slices share no graph node or edge. Use `--json` for machine output. +Do not scrape those related-framework bullets into `related-to`. + + ## Node IDs IDs are public contracts. @@ -113,8 +120,11 @@ The first seed is `proposed` on purpose. ## Follow-up -1. Map one framework per PR (`feat/security-map-`). -2. Steward review for security semantics. -3. Sourced incidents in dedicated evidence PRs. -4. Coverage reports: threats without controls, controls without guidance, critical assets without response, broken routes. +1. Run `pnpm run coverage:security-map` and treat the lists as the work queue. +2. Map one framework per PR (`feat/security-map-`). Steward review for security semantics. + Wallet Security is the first expansion. Next implied gaps: Physical Security, then Treasury Operations. +3. Add the missing component, surface, control, or guidance edge. Cross-links appear because two + frameworks share a node, not because overview pages mention each other. +4. Sourced incidents in dedicated evidence PRs. 5. Schema migrations before changing stable IDs or enum meanings. + diff --git a/docs/data/security-map/edges/wallet-security.json b/docs/data/security-map/edges/wallet-security.json new file mode 100644 index 00000000..6722e9ed --- /dev/null +++ b/docs/data/security-map/edges/wallet-security.json @@ -0,0 +1,229 @@ +{ + "edges": [ + { + "source": "component-hardware-wallet", + "target": "surface-transaction-signing", + "type": "exposes" + }, + { + "source": "component-software-wallet", + "target": "surface-transaction-signing", + "type": "exposes" + }, + { + "source": "component-software-wallet", + "target": "surface-token-approvals", + "type": "exposes" + }, + { + "source": "component-seed-backup", + "target": "surface-seed-custody", + "type": "exposes" + }, + { + "source": "component-signers", + "target": "component-hardware-wallet", + "type": "depends-on" + }, + { + "source": "component-multisig", + "target": "component-hardware-wallet", + "type": "depends-on" + }, + { + "source": "component-software-wallet", + "target": "component-frontend", + "type": "depends-on" + }, + { + "source": "threat-blind-signing", + "target": "surface-transaction-signing", + "type": "targets" + }, + { + "source": "threat-blind-signing", + "target": "asset-signer-keys", + "type": "targets" + }, + { + "source": "threat-blind-signing", + "target": "asset-user-funds", + "type": "targets" + }, + { + "source": "threat-blind-signing", + "target": "asset-treasury-funds", + "type": "targets" + }, + { + "source": "threat-seed-phrase-compromise", + "target": "surface-seed-custody", + "type": "targets" + }, + { + "source": "threat-seed-phrase-compromise", + "target": "asset-seed-secrets", + "type": "targets" + }, + { + "source": "threat-seed-phrase-compromise", + "target": "asset-signer-keys", + "type": "targets" + }, + { + "source": "threat-unlimited-token-approval", + "target": "surface-token-approvals", + "type": "targets" + }, + { + "source": "threat-unlimited-token-approval", + "target": "asset-user-funds", + "type": "targets" + }, + { + "source": "threat-duress-coercion", + "target": "asset-seed-secrets", + "type": "targets" + }, + { + "source": "threat-duress-coercion", + "target": "surface-seed-custody", + "type": "targets" + }, + { + "source": "control-simulate-before-sign", + "target": "threat-blind-signing", + "type": "mitigates" + }, + { + "source": "control-independent-tx-verification", + "target": "threat-blind-signing", + "type": "mitigates" + }, + { + "source": "control-signer-isolation", + "target": "threat-blind-signing", + "type": "mitigates" + }, + { + "source": "control-cold-wallet-separation", + "target": "threat-blind-signing", + "type": "mitigates" + }, + { + "source": "control-offline-seed-custody", + "target": "threat-seed-phrase-compromise", + "type": "mitigates" + }, + { + "source": "control-offline-seed-custody", + "target": "threat-duress-coercion", + "type": "mitigates" + }, + { + "source": "control-limited-token-approvals", + "target": "threat-unlimited-token-approval", + "type": "mitigates" + }, + { + "source": "control-cold-wallet-separation", + "target": "asset-signer-keys", + "type": "protects" + }, + { + "source": "control-cold-wallet-separation", + "target": "component-hardware-wallet", + "type": "protects" + }, + { + "source": "control-offline-seed-custody", + "target": "asset-seed-secrets", + "type": "protects" + }, + { + "source": "control-offline-seed-custody", + "target": "surface-seed-custody", + "type": "protects" + }, + { + "source": "control-simulate-before-sign", + "target": "surface-transaction-signing", + "type": "protects" + }, + { + "source": "control-limited-token-approvals", + "target": "surface-token-approvals", + "type": "protects" + }, + { + "source": "control-signer-isolation", + "target": "component-hardware-wallet", + "type": "protects" + }, + { + "source": "control-independent-tx-verification", + "target": "surface-transaction-signing", + "type": "protects" + }, + { + "source": "control-signer-isolation", + "target": "guidance-cold-vs-hot", + "type": "documented-by" + }, + { + "source": "control-cold-wallet-separation", + "target": "guidance-cold-vs-hot", + "type": "documented-by" + }, + { + "source": "control-offline-seed-custody", + "target": "guidance-seed-phrase-management", + "type": "documented-by" + }, + { + "source": "control-simulate-before-sign", + "target": "guidance-signing-verification", + "type": "documented-by" + }, + { + "source": "control-independent-tx-verification", + "target": "guidance-signing-verification", + "type": "documented-by" + }, + { + "source": "control-limited-token-approvals", + "target": "guidance-smart-contract-interaction", + "type": "documented-by" + }, + { + "source": "threat-blind-signing", + "target": "guidance-signing-verification", + "type": "documented-by" + }, + { + "source": "threat-seed-phrase-compromise", + "target": "guidance-seed-phrase-management", + "type": "documented-by" + }, + { + "source": "threat-unlimited-token-approval", + "target": "guidance-smart-contract-interaction", + "type": "documented-by" + }, + { + "source": "component-hardware-wallet", + "target": "guidance-cold-vs-hot", + "type": "documented-by" + }, + { + "source": "asset-seed-secrets", + "target": "guidance-seed-phrase-management", + "type": "documented-by" + }, + { + "source": "component-software-wallet", + "target": "guidance-wallet-security", + "type": "documented-by" + } + ] +} diff --git a/docs/data/security-map/nodes/wallet-security.json b/docs/data/security-map/nodes/wallet-security.json new file mode 100644 index 00000000..f8e9e523 --- /dev/null +++ b/docs/data/security-map/nodes/wallet-security.json @@ -0,0 +1,235 @@ +{ + "nodes": [ + { + "id": "asset-seed-secrets", + "type": "asset", + "title": "Seed phrase and recovery secrets", + "summary": "The mnemonic or recovery material that reconstructs signing keys. Anyone who sees it can take the wallet.", + "domains": ["devices-identity", "people"], + "status": "proposed", + "tags": ["seed-phrase", "recovery", "keys"], + "roles": ["multisig-signer"], + "lifecycle": ["normal-operations", "incident-response"], + "framework": "wallet-security" + }, + { + "id": "component-hardware-wallet", + "type": "component", + "title": "Hardware wallet", + "summary": "A dedicated signing device that keeps keys offline and shows destination and amount on its own screen.", + "domains": ["devices-identity", "governance-treasury"], + "status": "proposed", + "tags": ["hardware-wallet", "cold-wallet"], + "roles": ["multisig-signer"], + "lifecycle": ["normal-operations"], + "framework": "wallet-security" + }, + { + "id": "component-software-wallet", + "type": "component", + "title": "Software wallet", + "summary": "A hot wallet on a phone or browser that signs from an internet-connected device. Convenience with a larger remote attack surface.", + "domains": ["devices-identity", "onchain-systems"], + "status": "proposed", + "tags": ["software-wallet", "hot-wallet"], + "roles": ["multisig-signer"], + "lifecycle": ["normal-operations"], + "framework": "wallet-security" + }, + { + "id": "component-seed-backup", + "type": "component", + "title": "Seed backup", + "summary": "Offline copies of the seed phrase: paper, metal, or split shares. Physical access here is wallet takeover.", + "domains": ["devices-identity", "people"], + "status": "proposed", + "tags": ["seed-phrase", "backup"], + "roles": ["multisig-signer"], + "lifecycle": ["normal-operations", "recovery"], + "framework": "wallet-security" + }, + { + "id": "surface-transaction-signing", + "type": "attack-surface", + "title": "Transaction signing", + "summary": "The moment a wallet approves calldata. A hostile UI or unread payload turns a good key into an authorized drain.", + "domains": ["devices-identity", "onchain-systems"], + "status": "proposed", + "tags": ["signing", "calldata"], + "roles": ["multisig-signer"], + "lifecycle": ["normal-operations"], + "framework": "wallet-security" + }, + { + "id": "surface-seed-custody", + "type": "attack-surface", + "title": "Seed custody", + "summary": "How the seed is written, stored, split, and retrieved. Digital copies and casual physical access both count as exposure.", + "domains": ["devices-identity", "people"], + "status": "proposed", + "tags": ["seed-phrase", "custody"], + "roles": ["multisig-signer"], + "lifecycle": ["normal-operations", "recovery"], + "framework": "wallet-security" + }, + { + "id": "surface-token-approvals", + "type": "attack-surface", + "title": "Token approvals", + "summary": "ERC-20 approve and permit grants that let a spender move tokens later. Unlimited allowance outlives the original swap.", + "domains": ["onchain-systems"], + "status": "proposed", + "tags": ["approvals", "permit", "erc20"], + "roles": ["engineer-developer", "multisig-signer"], + "lifecycle": ["normal-operations"], + "framework": "wallet-security" + }, + { + "id": "threat-blind-signing", + "type": "threat", + "title": "Blind signing", + "summary": "The signer approves attacker-controlled calldata because a website, chat, or wallet UI was trusted instead of the raw transaction.", + "domains": ["devices-identity", "onchain-systems"], + "status": "proposed", + "tags": ["signing", "phishing", "calldata"], + "severity": "critical", + "severityBasis": "Default triage is critical because a valid signature spends funds without stealing the key. Impact scales with what the wallet can move.", + "framework": "wallet-security" + }, + { + "id": "threat-seed-phrase-compromise", + "type": "threat", + "title": "Seed phrase compromise", + "summary": "The seed is photographed, typed into a connected device, stored in cloud notes, or read by someone with physical access.", + "domains": ["devices-identity", "people"], + "status": "proposed", + "tags": ["seed-phrase", "theft"], + "severity": "critical", + "severityBasis": "Default triage is critical because the seed reconstructs every derived key. Treat any suspected exposure as full compromise.", + "framework": "wallet-security" + }, + { + "id": "threat-unlimited-token-approval", + "type": "threat", + "title": "Unlimited token approval", + "summary": "A spender keeps uint256 allowance or a permit grant after the intended swap. A later exploit or hostile spender drains the wallet.", + "domains": ["onchain-systems"], + "status": "proposed", + "tags": ["approvals", "permit"], + "severity": "high", + "severityBasis": "Default triage is high because the drain does not need the key, but it is limited to the approved token and spender. Treat as critical when the spender is unaudited or the allowance is protocol-wide.", + "framework": "wallet-security" + }, + { + "id": "control-cold-wallet-separation", + "type": "control", + "title": "Cold and hot wallet separation", + "summary": "Keep high-value keys on offline or hardware signers. Use a hot wallet only for amounts you can afford to lose.", + "domains": ["devices-identity", "governance-treasury"], + "status": "proposed", + "tags": ["cold-wallet", "isolation"], + "controlClass": "preventive", + "assessmentEligible": true, + "roles": ["multisig-signer"], + "lifecycle": ["normal-operations"], + "framework": "wallet-security" + }, + { + "id": "control-offline-seed-custody", + "type": "control", + "title": "Offline seed custody", + "summary": "Keep the seed offline, preferably durable and split. Treat any screen, photo, or cloud copy as compromise and rotate.", + "domains": ["devices-identity", "people"], + "status": "proposed", + "tags": ["seed-phrase", "offline"], + "controlClass": "preventive", + "assessmentEligible": true, + "roles": ["multisig-signer"], + "lifecycle": ["normal-operations", "recovery"], + "framework": "wallet-security" + }, + { + "id": "control-simulate-before-sign", + "type": "control", + "title": "Simulate and decode before signing", + "summary": "Independently simulate the transaction and read destination, asset, and calldata on a trusted display. Do not sign because a website said it was safe.", + "domains": ["devices-identity", "onchain-systems"], + "status": "proposed", + "tags": ["simulation", "verification"], + "controlClass": "preventive", + "assessmentEligible": true, + "roles": ["multisig-signer"], + "lifecycle": ["normal-operations"], + "framework": "wallet-security" + }, + { + "id": "control-limited-token-approvals", + "type": "control", + "title": "Limited token approvals", + "summary": "Approve only the amount needed for this transaction. Revoke leftover allowances. Avoid permit and infinite approve by default.", + "domains": ["onchain-systems"], + "status": "proposed", + "tags": ["approvals", "allowance"], + "controlClass": "preventive", + "assessmentEligible": true, + "roles": ["engineer-developer", "multisig-signer"], + "lifecycle": ["normal-operations"], + "framework": "wallet-security" + }, + { + "id": "guidance-wallet-security", + "type": "guidance", + "title": "Wallet Security", + "summary": "Custody models, hot versus cold tradeoffs, signing verification, seed custody, and safer contract interaction.", + "domains": ["devices-identity", "onchain-systems"], + "status": "proposed", + "tags": ["wallet"], + "href": "/wallet-security/overview", + "framework": "wallet-security" + }, + { + "id": "guidance-cold-vs-hot", + "type": "guidance", + "title": "Cold vs hot wallets", + "summary": "Match fund tier to connectivity. Cold and hardware signers cut remote theft; hot wallets stay for operational amounts.", + "domains": ["devices-identity"], + "status": "proposed", + "tags": ["cold-wallet", "hot-wallet"], + "href": "/wallet-security/cold-vs-hot-wallet", + "framework": "wallet-security" + }, + { + "id": "guidance-seed-phrase-management", + "type": "guidance", + "title": "Seed phrase management", + "summary": "Offline backup, metal and split-share options, and the rule that suspected exposure means rotate immediately.", + "domains": ["devices-identity", "people"], + "status": "proposed", + "tags": ["seed-phrase"], + "href": "/wallet-security/seed-phrase-management", + "framework": "wallet-security" + }, + { + "id": "guidance-signing-verification", + "type": "guidance", + "title": "Signing and verification", + "summary": "Never sign blindly. Prefer the hardware screen and independently verified calldata over any web UI claim.", + "domains": ["devices-identity", "onchain-systems"], + "status": "proposed", + "tags": ["signing", "verification"], + "href": "/wallet-security/signing-and-verification/signing-verification", + "framework": "wallet-security" + }, + { + "id": "guidance-smart-contract-interaction", + "type": "guidance", + "title": "Smart contract interaction security", + "summary": "Verify the contract, simulate, and limit approvals before interacting. Most losses here are interaction mistakes, not stolen keys.", + "domains": ["onchain-systems"], + "status": "proposed", + "tags": ["approvals", "simulation"], + "href": "/wallet-security/smart-contract-interaction-security", + "framework": "wallet-security" + } + ] +} diff --git a/package.json b/package.json index 1fa7427b..e037c34a 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "generate-cert-data": "node utils/generate-cert-data.cjs", "generate-security-map": "node utils/generate-security-map.cjs", "validate:security-map": "node utils/validate-security-map.cjs", + "coverage:security-map": "node utils/coverage-security-map.cjs", "test:security-map": "node --test utils/security-map.test.cjs utils/security-map-assessment.test.cjs", + "validate:content": "node utils/validate-content.cjs" }, "keywords": [], diff --git a/public/security-map.json b/public/security-map.json index ab25e710..dce601b9 100644 --- a/public/security-map.json +++ b/public/security-map.json @@ -114,6 +114,30 @@ "normal-operations" ] }, + { + "id": "asset-seed-secrets", + "type": "asset", + "title": "Seed phrase and recovery secrets", + "summary": "The mnemonic or recovery material that reconstructs signing keys. Anyone who sees it can take the wallet.", + "domains": [ + "devices-identity", + "people" + ], + "status": "proposed", + "tags": [ + "seed-phrase", + "recovery", + "keys" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations", + "incident-response" + ], + "framework": "wallet-security" + }, { "id": "asset-signer-keys", "type": "asset", @@ -392,6 +416,28 @@ ], "framework": "governance" }, + { + "id": "component-hardware-wallet", + "type": "component", + "title": "Hardware wallet", + "summary": "A dedicated signing device that keeps keys offline and shows destination and amount on its own screen.", + "domains": [ + "devices-identity", + "governance-treasury" + ], + "status": "proposed", + "tags": [ + "hardware-wallet", + "cold-wallet" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations" + ], + "framework": "wallet-security" + }, { "id": "component-hiring-pipeline", "type": "component", @@ -503,6 +549,29 @@ "normal-operations" ] }, + { + "id": "component-seed-backup", + "type": "component", + "title": "Seed backup", + "summary": "Offline copies of the seed phrase: paper, metal, or split shares. Physical access here is wallet takeover.", + "domains": [ + "devices-identity", + "people" + ], + "status": "proposed", + "tags": [ + "seed-phrase", + "backup" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations", + "recovery" + ], + "framework": "wallet-security" + }, { "id": "component-signers", "type": "component", @@ -547,6 +616,28 @@ ], "framework": "external-security-reviews" }, + { + "id": "component-software-wallet", + "type": "component", + "title": "Software wallet", + "summary": "A hot wallet on a phone or browser that signs from an internet-connected device. Convenience with a larger remote attack surface.", + "domains": [ + "devices-identity", + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "software-wallet", + "hot-wallet" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations" + ], + "framework": "wallet-security" + }, { "id": "control-branch-protection", "type": "control", @@ -593,6 +684,30 @@ "assessmentEligible": true, "framework": "infrastructure" }, + { + "id": "control-cold-wallet-separation", + "type": "control", + "title": "Cold and hot wallet separation", + "summary": "Keep high-value keys on offline or hardware signers. Use a hot wallet only for amounts you can afford to lose.", + "domains": [ + "devices-identity", + "governance-treasury" + ], + "status": "proposed", + "tags": [ + "cold-wallet", + "isolation" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations" + ], + "controlClass": "preventive", + "assessmentEligible": true, + "framework": "wallet-security" + }, { "id": "control-dependency-pinning-provenance", "type": "control", @@ -884,6 +999,30 @@ "assessmentEligible": true, "framework": "iam" }, + { + "id": "control-limited-token-approvals", + "type": "control", + "title": "Limited token approvals", + "summary": "Approve only the amount needed for this transaction. Revoke leftover allowances. Avoid permit and infinite approve by default.", + "domains": [ + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "approvals", + "allowance" + ], + "roles": [ + "engineer-developer", + "multisig-signer" + ], + "lifecycle": [ + "normal-operations" + ], + "controlClass": "preventive", + "assessmentEligible": true, + "framework": "wallet-security" + }, { "id": "control-multisig-threshold-policy", "type": "control", @@ -933,6 +1072,31 @@ "assessmentEligible": true, "framework": "infrastructure" }, + { + "id": "control-offline-seed-custody", + "type": "control", + "title": "Offline seed custody", + "summary": "Keep the seed offline, preferably durable and split. Treat any screen, photo, or cloud copy as compromise and rotate.", + "domains": [ + "devices-identity", + "people" + ], + "status": "proposed", + "tags": [ + "seed-phrase", + "offline" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations", + "recovery" + ], + "controlClass": "preventive", + "assessmentEligible": true, + "framework": "wallet-security" + }, { "id": "control-onchain-anomaly-detection", "type": "control", @@ -1128,6 +1292,30 @@ "assessmentEligible": true, "framework": "multisig-for-protocols" }, + { + "id": "control-simulate-before-sign", + "type": "control", + "title": "Simulate and decode before signing", + "summary": "Independently simulate the transaction and read destination, asset, and calldata on a trusted display. Do not sign because a website said it was safe.", + "domains": [ + "devices-identity", + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "simulation", + "verification" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations" + ], + "controlClass": "preventive", + "assessmentEligible": true, + "framework": "wallet-security" + }, { "id": "control-tested-incident-runbooks", "type": "control", @@ -1212,6 +1400,22 @@ "href": "/devsecops/code-signing", "framework": "devsecops" }, + { + "id": "guidance-cold-vs-hot", + "type": "guidance", + "title": "Cold vs hot wallets", + "summary": "Match fund tier to connectivity. Cold and hardware signers cut remote theft; hot wallets stay for operational amounts.", + "domains": [ + "devices-identity" + ], + "status": "proposed", + "tags": [ + "cold-wallet", + "hot-wallet" + ], + "href": "/wallet-security/cold-vs-hot-wallet", + "framework": "wallet-security" + }, { "id": "guidance-community", "type": "guidance", @@ -1734,6 +1938,55 @@ "href": "/user-team-security/security-training", "framework": "user-team-security" }, + { + "id": "guidance-seed-phrase-management", + "type": "guidance", + "title": "Seed phrase management", + "summary": "Offline backup, metal and split-share options, and the rule that suspected exposure means rotate immediately.", + "domains": [ + "devices-identity", + "people" + ], + "status": "proposed", + "tags": [ + "seed-phrase" + ], + "href": "/wallet-security/seed-phrase-management", + "framework": "wallet-security" + }, + { + "id": "guidance-signing-verification", + "type": "guidance", + "title": "Signing and verification", + "summary": "Never sign blindly. Prefer the hardware screen and independently verified calldata over any web UI claim.", + "domains": [ + "devices-identity", + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "signing", + "verification" + ], + "href": "/wallet-security/signing-and-verification/signing-verification", + "framework": "wallet-security" + }, + { + "id": "guidance-smart-contract-interaction", + "type": "guidance", + "title": "Smart contract interaction security", + "summary": "Verify the contract, simulate, and limit approvals before interacting. Most losses here are interaction mistakes, not stolen keys.", + "domains": [ + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "approvals", + "simulation" + ], + "href": "/wallet-security/smart-contract-interaction-security", + "framework": "wallet-security" + }, { "id": "guidance-supply-chain-overview", "type": "guidance", @@ -1814,6 +2067,22 @@ "href": "/opsec/travel/guide", "framework": "opsec" }, + { + "id": "guidance-wallet-security", + "type": "guidance", + "title": "Wallet Security", + "summary": "Custody models, hot versus cold tradeoffs, signing verification, seed custody, and safer contract interaction.", + "domains": [ + "devices-identity", + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "wallet" + ], + "href": "/wallet-security/overview", + "framework": "wallet-security" + }, { "id": "guidance-zero-trust", "type": "guidance", @@ -2244,6 +2513,29 @@ "normal-operations" ] }, + { + "id": "surface-seed-custody", + "type": "attack-surface", + "title": "Seed custody", + "summary": "How the seed is written, stored, split, and retrieved. Digital copies and casual physical access both count as exposure.", + "domains": [ + "devices-identity", + "people" + ], + "status": "proposed", + "tags": [ + "seed-phrase", + "custody" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations", + "recovery" + ], + "framework": "wallet-security" + }, { "id": "surface-signer-lifecycle", "type": "attack-surface", @@ -2265,6 +2557,70 @@ "normal-operations" ] }, + { + "id": "surface-token-approvals", + "type": "attack-surface", + "title": "Token approvals", + "summary": "ERC-20 approve and permit grants that let a spender move tokens later. Unlimited allowance outlives the original swap.", + "domains": [ + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "approvals", + "permit", + "erc20" + ], + "roles": [ + "engineer-developer", + "multisig-signer" + ], + "lifecycle": [ + "normal-operations" + ], + "framework": "wallet-security" + }, + { + "id": "surface-transaction-signing", + "type": "attack-surface", + "title": "Transaction signing", + "summary": "The moment a wallet approves calldata. A hostile UI or unread payload turns a good key into an authorized drain.", + "domains": [ + "devices-identity", + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "signing", + "calldata" + ], + "roles": [ + "multisig-signer" + ], + "lifecycle": [ + "normal-operations" + ], + "framework": "wallet-security" + }, + { + "id": "threat-blind-signing", + "type": "threat", + "title": "Blind signing", + "summary": "The signer approves attacker-controlled calldata because a website, chat, or wallet UI was trusted instead of the raw transaction.", + "domains": [ + "devices-identity", + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "signing", + "phishing", + "calldata" + ], + "severity": "critical", + "severityBasis": "Default triage is critical because a valid signature spends funds without stealing the key. Impact scales with what the wallet can move.", + "framework": "wallet-security" + }, { "id": "threat-dprk-it-worker-infiltration", "type": "threat", @@ -2455,6 +2811,24 @@ ], "framework": "opsec" }, + { + "id": "threat-seed-phrase-compromise", + "type": "threat", + "title": "Seed phrase compromise", + "summary": "The seed is photographed, typed into a connected device, stored in cloud notes, or read by someone with physical access.", + "domains": [ + "devices-identity", + "people" + ], + "status": "proposed", + "tags": [ + "seed-phrase", + "theft" + ], + "severity": "critical", + "severityBasis": "Default triage is critical because the seed reconstructs every derived key. Treat any suspected exposure as full compromise.", + "framework": "wallet-security" + }, { "id": "threat-smart-contract-exploits", "type": "threat", @@ -2518,6 +2892,23 @@ "supply-chain-attacks" ], "framework": "supply-chain" + }, + { + "id": "threat-unlimited-token-approval", + "type": "threat", + "title": "Unlimited token approval", + "summary": "A spender keeps uint256 allowance or a permit grant after the intended swap. A later exploit or hostile spender drains the wallet.", + "domains": [ + "onchain-systems" + ], + "status": "proposed", + "tags": [ + "approvals", + "permit" + ], + "severity": "high", + "severityBasis": "Default triage is high because the drain does not need the key, but it is limited to the approved token and spender. Treat as critical when the spender is unaudited or the allowance is protocol-wide.", + "framework": "wallet-security" } ], "edges": [ @@ -2593,6 +2984,13 @@ "type": "depends-on", "status": "proposed" }, + { + "id": "depends-on:component-multisig:component-hardware-wallet", + "source": "component-multisig", + "target": "component-hardware-wallet", + "type": "depends-on", + "status": "proposed" + }, { "id": "depends-on:component-multisig:component-signers", "source": "component-multisig", @@ -2600,6 +2998,13 @@ "type": "depends-on", "status": "proposed" }, + { + "id": "depends-on:component-signers:component-hardware-wallet", + "source": "component-signers", + "target": "component-hardware-wallet", + "type": "depends-on", + "status": "proposed" + }, { "id": "depends-on:component-smart-contracts:component-multisig", "source": "component-smart-contracts", @@ -2608,6 +3013,13 @@ "rationale": "Upgradeable contracts typically grant admin to a protocol multisig.", "status": "proposed" }, + { + "id": "depends-on:component-software-wallet:component-frontend", + "source": "component-software-wallet", + "target": "component-frontend", + "type": "depends-on", + "status": "proposed" + }, { "id": "detects:control-dns-change-monitoring:threat-frontend-dns-hijacking", "source": "control-dns-change-monitoring", @@ -2643,6 +3055,27 @@ "type": "detects", "status": "proposed" }, + { + "id": "documented-by:asset-seed-secrets:guidance-seed-phrase-management", + "source": "asset-seed-secrets", + "target": "guidance-seed-phrase-management", + "type": "documented-by", + "status": "proposed" + }, + { + "id": "documented-by:component-hardware-wallet:guidance-cold-vs-hot", + "source": "component-hardware-wallet", + "target": "guidance-cold-vs-hot", + "type": "documented-by", + "status": "proposed" + }, + { + "id": "documented-by:component-software-wallet:guidance-wallet-security", + "source": "component-software-wallet", + "target": "guidance-wallet-security", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:control-branch-protection:guidance-devsecops", "source": "control-branch-protection", @@ -2657,6 +3090,13 @@ "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:control-cold-wallet-separation:guidance-cold-vs-hot", + "source": "control-cold-wallet-separation", + "target": "guidance-cold-vs-hot", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:control-dependency-pinning-provenance:guidance-supply-chain-overview", "source": "control-dependency-pinning-provenance", @@ -2741,6 +3181,13 @@ "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:control-independent-tx-verification:guidance-signing-verification", + "source": "control-independent-tx-verification", + "target": "guidance-signing-verification", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:control-insider-threat-mitigation:guidance-insider-threat", "source": "control-insider-threat-mitigation", @@ -2769,6 +3216,13 @@ "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:control-limited-token-approvals:guidance-smart-contract-interaction", + "source": "control-limited-token-approvals", + "target": "guidance-smart-contract-interaction", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:control-multisig-threshold-policy:guidance-multisig-overview", "source": "control-multisig-threshold-policy", @@ -2790,6 +3244,13 @@ "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:control-offline-seed-custody:guidance-seed-phrase-management", + "source": "control-offline-seed-custody", + "target": "guidance-seed-phrase-management", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:control-onchain-anomaly-detection:guidance-threat-detection", "source": "control-onchain-anomaly-detection", @@ -2853,6 +3314,13 @@ "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:control-signer-isolation:guidance-cold-vs-hot", + "source": "control-signer-isolation", + "target": "guidance-cold-vs-hot", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:control-signer-isolation:guidance-opsec", "source": "control-signer-isolation", @@ -2867,6 +3335,13 @@ "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:control-simulate-before-sign:guidance-signing-verification", + "source": "control-simulate-before-sign", + "target": "guidance-signing-verification", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:control-tested-incident-runbooks:guidance-incident-playbooks", "source": "control-tested-incident-runbooks", @@ -2923,6 +3398,13 @@ "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:threat-blind-signing:guidance-signing-verification", + "source": "threat-blind-signing", + "target": "guidance-signing-verification", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:threat-dprk-it-worker-infiltration:guidance-dprk-playbook", "source": "threat-dprk-it-worker-infiltration", @@ -2965,6 +3447,13 @@ "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:threat-seed-phrase-compromise:guidance-seed-phrase-management", + "source": "threat-seed-phrase-compromise", + "target": "guidance-seed-phrase-management", + "type": "documented-by", + "status": "proposed" + }, { "id": "documented-by:threat-smart-contract-exploits:guidance-external-security-reviews", "source": "threat-smart-contract-exploits", @@ -2986,6 +3475,13 @@ "type": "documented-by", "status": "proposed" }, + { + "id": "documented-by:threat-unlimited-token-approval:guidance-smart-contract-interaction", + "source": "threat-unlimited-token-approval", + "target": "guidance-smart-contract-interaction", + "type": "documented-by", + "status": "proposed" + }, { "id": "exposes:component-ci-cd:surface-ci-cd-secrets", "source": "component-ci-cd", @@ -3042,6 +3538,13 @@ "type": "exposes", "status": "proposed" }, + { + "id": "exposes:component-hardware-wallet:surface-transaction-signing", + "source": "component-hardware-wallet", + "target": "surface-transaction-signing", + "type": "exposes", + "status": "proposed" + }, { "id": "exposes:component-hiring-pipeline:surface-hiring-contractor-access", "source": "component-hiring-pipeline", @@ -3070,6 +3573,13 @@ "type": "exposes", "status": "proposed" }, + { + "id": "exposes:component-seed-backup:surface-seed-custody", + "source": "component-seed-backup", + "target": "surface-seed-custody", + "type": "exposes", + "status": "proposed" + }, { "id": "exposes:component-signers:surface-physical-presence", "source": "component-signers", @@ -3098,6 +3608,20 @@ "type": "exposes", "status": "proposed" }, + { + "id": "exposes:component-software-wallet:surface-token-approvals", + "source": "component-software-wallet", + "target": "surface-token-approvals", + "type": "exposes", + "status": "proposed" + }, + { + "id": "exposes:component-software-wallet:surface-transaction-signing", + "source": "component-software-wallet", + "target": "surface-transaction-signing", + "type": "exposes", + "status": "proposed" + }, { "id": "mitigates:control-branch-protection:threat-supply-chain-compromise", "source": "control-branch-protection", @@ -3112,6 +3636,13 @@ "type": "mitigates", "status": "proposed" }, + { + "id": "mitigates:control-cold-wallet-separation:threat-blind-signing", + "source": "control-cold-wallet-separation", + "target": "threat-blind-signing", + "type": "mitigates", + "status": "proposed" + }, { "id": "mitigates:control-dependency-pinning-provenance:threat-supply-chain-compromise", "source": "control-dependency-pinning-provenance", @@ -3161,6 +3692,13 @@ "type": "mitigates", "status": "proposed" }, + { + "id": "mitigates:control-independent-tx-verification:threat-blind-signing", + "source": "control-independent-tx-verification", + "target": "threat-blind-signing", + "type": "mitigates", + "status": "proposed" + }, { "id": "mitigates:control-independent-tx-verification:threat-governance-manipulation", "source": "control-independent-tx-verification", @@ -3210,6 +3748,13 @@ "type": "mitigates", "status": "proposed" }, + { + "id": "mitigates:control-limited-token-approvals:threat-unlimited-token-approval", + "source": "control-limited-token-approvals", + "target": "threat-unlimited-token-approval", + "type": "mitigates", + "status": "proposed" + }, { "id": "mitigates:control-multisig-threshold-policy:threat-duress-coercion", "source": "control-multisig-threshold-policy", @@ -3231,6 +3776,20 @@ "type": "mitigates", "status": "proposed" }, + { + "id": "mitigates:control-offline-seed-custody:threat-duress-coercion", + "source": "control-offline-seed-custody", + "target": "threat-duress-coercion", + "type": "mitigates", + "status": "proposed" + }, + { + "id": "mitigates:control-offline-seed-custody:threat-seed-phrase-compromise", + "source": "control-offline-seed-custody", + "target": "threat-seed-phrase-compromise", + "type": "mitigates", + "status": "proposed" + }, { "id": "mitigates:control-phishing-resistant-mfa:threat-leadership-phishing", "source": "control-phishing-resistant-mfa", @@ -3280,6 +3839,13 @@ "type": "mitigates", "status": "proposed" }, + { + "id": "mitigates:control-signer-isolation:threat-blind-signing", + "source": "control-signer-isolation", + "target": "threat-blind-signing", + "type": "mitigates", + "status": "proposed" + }, { "id": "mitigates:control-signer-isolation:threat-leadership-phishing", "source": "control-signer-isolation", @@ -3301,6 +3867,13 @@ "type": "mitigates", "status": "proposed" }, + { + "id": "mitigates:control-simulate-before-sign:threat-blind-signing", + "source": "control-simulate-before-sign", + "target": "threat-blind-signing", + "type": "mitigates", + "status": "proposed" + }, { "id": "mitigates:control-tested-incident-runbooks:threat-monitoring-gaps", "source": "control-tested-incident-runbooks", @@ -3322,6 +3895,20 @@ "type": "protects", "status": "proposed" }, + { + "id": "protects:control-cold-wallet-separation:asset-signer-keys", + "source": "control-cold-wallet-separation", + "target": "asset-signer-keys", + "type": "protects", + "status": "proposed" + }, + { + "id": "protects:control-cold-wallet-separation:component-hardware-wallet", + "source": "control-cold-wallet-separation", + "target": "component-hardware-wallet", + "type": "protects", + "status": "proposed" + }, { "id": "protects:control-external-security-review:component-smart-contracts", "source": "control-external-security-review", @@ -3343,6 +3930,13 @@ "type": "protects", "status": "proposed" }, + { + "id": "protects:control-independent-tx-verification:surface-transaction-signing", + "source": "control-independent-tx-verification", + "target": "surface-transaction-signing", + "type": "protects", + "status": "proposed" + }, { "id": "protects:control-isolated-reproducible-builds:asset-deployment-authority", "source": "control-isolated-reproducible-builds", @@ -3350,6 +3944,13 @@ "type": "protects", "status": "proposed" }, + { + "id": "protects:control-limited-token-approvals:surface-token-approvals", + "source": "control-limited-token-approvals", + "target": "surface-token-approvals", + "type": "protects", + "status": "proposed" + }, { "id": "protects:control-multisig-threshold-policy:asset-treasury-funds", "source": "control-multisig-threshold-policy", @@ -3357,6 +3958,20 @@ "type": "protects", "status": "proposed" }, + { + "id": "protects:control-offline-seed-custody:asset-seed-secrets", + "source": "control-offline-seed-custody", + "target": "asset-seed-secrets", + "type": "protects", + "status": "proposed" + }, + { + "id": "protects:control-offline-seed-custody:surface-seed-custody", + "source": "control-offline-seed-custody", + "target": "surface-seed-custody", + "type": "protects", + "status": "proposed" + }, { "id": "protects:control-phishing-resistant-mfa:component-identity-accounts", "source": "control-phishing-resistant-mfa", @@ -3378,6 +3993,20 @@ "type": "protects", "status": "proposed" }, + { + "id": "protects:control-signer-isolation:component-hardware-wallet", + "source": "control-signer-isolation", + "target": "component-hardware-wallet", + "type": "protects", + "status": "proposed" + }, + { + "id": "protects:control-simulate-before-sign:surface-transaction-signing", + "source": "control-simulate-before-sign", + "target": "surface-transaction-signing", + "type": "protects", + "status": "proposed" + }, { "id": "responds-to:response-dns-hijack:threat-frontend-dns-hijacking", "source": "response-dns-hijack", @@ -3427,6 +4056,34 @@ "type": "responds-to", "status": "proposed" }, + { + "id": "targets:threat-blind-signing:asset-signer-keys", + "source": "threat-blind-signing", + "target": "asset-signer-keys", + "type": "targets", + "status": "proposed" + }, + { + "id": "targets:threat-blind-signing:asset-treasury-funds", + "source": "threat-blind-signing", + "target": "asset-treasury-funds", + "type": "targets", + "status": "proposed" + }, + { + "id": "targets:threat-blind-signing:asset-user-funds", + "source": "threat-blind-signing", + "target": "asset-user-funds", + "type": "targets", + "status": "proposed" + }, + { + "id": "targets:threat-blind-signing:surface-transaction-signing", + "source": "threat-blind-signing", + "target": "surface-transaction-signing", + "type": "targets", + "status": "proposed" + }, { "id": "targets:threat-dprk-it-worker-infiltration:asset-secrets-credentials", "source": "threat-dprk-it-worker-infiltration", @@ -3448,6 +4105,13 @@ "type": "targets", "status": "proposed" }, + { + "id": "targets:threat-duress-coercion:asset-seed-secrets", + "source": "threat-duress-coercion", + "target": "asset-seed-secrets", + "type": "targets", + "status": "proposed" + }, { "id": "targets:threat-duress-coercion:asset-signer-keys", "source": "threat-duress-coercion", @@ -3462,6 +4126,13 @@ "type": "targets", "status": "proposed" }, + { + "id": "targets:threat-duress-coercion:surface-seed-custody", + "source": "threat-duress-coercion", + "target": "surface-seed-custody", + "type": "targets", + "status": "proposed" + }, { "id": "targets:threat-frontend-dns-hijacking:asset-domain-control", "source": "threat-frontend-dns-hijacking", @@ -3602,6 +4273,27 @@ "type": "targets", "status": "proposed" }, + { + "id": "targets:threat-seed-phrase-compromise:asset-seed-secrets", + "source": "threat-seed-phrase-compromise", + "target": "asset-seed-secrets", + "type": "targets", + "status": "proposed" + }, + { + "id": "targets:threat-seed-phrase-compromise:asset-signer-keys", + "source": "threat-seed-phrase-compromise", + "target": "asset-signer-keys", + "type": "targets", + "status": "proposed" + }, + { + "id": "targets:threat-seed-phrase-compromise:surface-seed-custody", + "source": "threat-seed-phrase-compromise", + "target": "surface-seed-custody", + "type": "targets", + "status": "proposed" + }, { "id": "targets:threat-smart-contract-exploits:asset-treasury-funds", "source": "threat-smart-contract-exploits", @@ -3678,6 +4370,20 @@ "target": "surface-package-install", "type": "targets", "status": "proposed" + }, + { + "id": "targets:threat-unlimited-token-approval:asset-user-funds", + "source": "threat-unlimited-token-approval", + "target": "asset-user-funds", + "type": "targets", + "status": "proposed" + }, + { + "id": "targets:threat-unlimited-token-approval:surface-token-approvals", + "source": "threat-unlimited-token-approval", + "target": "surface-token-approvals", + "type": "targets", + "status": "proposed" } ], "taxonomies": { diff --git a/utils/coverage-security-map.cjs b/utils/coverage-security-map.cjs new file mode 100755 index 00000000..7c8a7182 --- /dev/null +++ b/utils/coverage-security-map.cjs @@ -0,0 +1,31 @@ +#!/usr/bin/env node +const { coverageReport, formatCoverage, loadAndValidate, ValidationError } = require('./security-map-lib.cjs') +const path = require('path') + +function main() { + const root = process.cwd() + const json = process.argv.includes('--json') + try { + const { graph } = loadAndValidate({ root }) + const pagesDir = path.join(root, 'docs', 'pages') + const report = coverageReport({ + nodes: graph.nodes, + edges: graph.edges, + pagesDir, + }) + if (json) { + console.log(JSON.stringify(report, null, 2)) + } else { + console.log(formatCoverage(report).trimEnd()) + } + } catch (err) { + if (err instanceof ValidationError || err.code === 'SECURITY_MAP_VALIDATION') { + console.error(err.message) + process.exit(1) + } + console.error(err) + process.exit(1) + } +} + +main() diff --git a/utils/security-map-lib.cjs b/utils/security-map-lib.cjs index b7fc467b..18045128 100755 --- a/utils/security-map-lib.cjs +++ b/utils/security-map-lib.cjs @@ -977,6 +977,170 @@ function generate(options = {}) { } } +function frameworkFromHref(href) { + if (typeof href !== 'string' || !href.startsWith('/') || href.startsWith('//')) return null + const first = href.replace(/^\//, '').split('/')[0] + return first || null +} + +function listFrameworkIds(pagesDir) { + if (!pagesDir || !fs.existsSync(pagesDir)) return [] + return fs + .readdirSync(pagesDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.')) + .filter((entry) => fs.existsSync(path.join(pagesDir, entry.name, 'overview.mdx'))) + .map((entry) => entry.name) + .sort() +} + +function relatedFrameworkSection(body) { + const lines = String(body || '').split('\n') + const start = lines.findIndex((line) => line.trim() === '## Related frameworks') + if (start < 0) return '' + const out = [] + for (let i = start + 1; i < lines.length; i += 1) { + if (/^## /.test(lines[i])) break + out.push(lines[i]) + } + return out.join('\n') +} + +function parseRelatedFrameworkHrefs(body) { + const section = relatedFrameworkSection(body) + if (!section) return [] + const hrefs = [] + const re = /\[[^\]]*\]\((\/[^)\s#]+)(?:#[^)]*)?\)/g + let match + while ((match = re.exec(section))) { + hrefs.push(match[1]) + } + return [...new Set(hrefs)] +} + +function nodeFrameworks(node) { + const out = new Set() + if (node.framework) out.add(node.framework) + const fromHref = frameworkFromHref(node.href) + if (fromHref) out.add(fromHref) + return out +} + +function sliceIds(nodes, frameworkId) { + return new Set(nodes.filter((node) => nodeFrameworks(node).has(frameworkId)).map((node) => node.id)) +} + +function slicesShare(idsA, idsB, edges) { + for (const id of idsA) { + if (idsB.has(id)) return true + } + for (const edge of edges) { + if ((idsA.has(edge.source) && idsB.has(edge.target)) || (idsB.has(edge.source) && idsA.has(edge.target))) { + return true + } + } + return false +} + +function coverageReport({ nodes, edges, pagesDir }) { + const graphNodes = nodes || [] + const graphEdges = edges || [] + + const threatsWithoutControl = graphNodes + .filter((node) => node.type === 'threat') + .filter((node) => !graphEdges.some((edge) => edge.type === 'mitigates' && edge.target === node.id)) + .map((node) => node.id) + .sort() + + const controlsWithoutGuidance = graphNodes + .filter((node) => node.type === 'control') + .filter((node) => !graphEdges.some((edge) => edge.type === 'documented-by' && edge.source === node.id)) + .map((node) => node.id) + .sort() + + const frameworks = listFrameworkIds(pagesDir) + const usedFrameworks = new Set() + for (const node of graphNodes) { + for (const fw of nodeFrameworks(node)) usedFrameworks.add(fw) + } + const frameworksWithZeroNodes = frameworks.filter((fw) => !usedFrameworks.has(fw)) + + const relatedUnshared = [] + if (pagesDir && fs.existsSync(pagesDir)) { + for (const file of walkMdx(pagesDir)) { + const raw = fs.readFileSync(file, 'utf8') + const parsed = matter(raw) + const hrefs = parseRelatedFrameworkHrefs(parsed.content) + if (!hrefs.length) continue + const pageHref = fileToHref(pagesDir, file) + const fromFw = frameworkFromHref(pageHref) + if (!fromFw) continue + const fromIds = sliceIds(graphNodes, fromFw) + for (const href of hrefs) { + const toFw = frameworkFromHref(href) + if (!toFw || toFw === fromFw) continue + const toIds = sliceIds(graphNodes, toFw) + if (slicesShare(fromIds, toIds, graphEdges)) continue + relatedUnshared.push({ + page: pageHref, + neighbor: href, + from: fromFw, + to: toFw, + }) + } + } + } + relatedUnshared.sort((a, b) => { + const left = `${a.page} ${a.neighbor}` + const right = `${b.page} ${b.neighbor}` + return left.localeCompare(right) + }) + + return { + threatsWithoutControl, + controlsWithoutGuidance, + frameworksWithZeroNodes, + relatedUnshared, + counts: { + nodes: graphNodes.length, + edges: graphEdges.length, + threatsWithoutControl: threatsWithoutControl.length, + controlsWithoutGuidance: controlsWithoutGuidance.length, + frameworksWithZeroNodes: frameworksWithZeroNodes.length, + relatedUnshared: relatedUnshared.length, + }, + } +} + +function formatCoverage(report) { + const lines = [ + 'Security map coverage', + '', + `Threats with no control (${report.counts.threatsWithoutControl})`, + ...(report.threatsWithoutControl.length + ? report.threatsWithoutControl.map((id) => `- ${id}`) + : ['- none']), + '', + `Controls with no guidance (${report.counts.controlsWithoutGuidance})`, + ...(report.controlsWithoutGuidance.length + ? report.controlsWithoutGuidance.map((id) => `- ${id}`) + : ['- none']), + '', + `Frameworks with 0 map nodes (${report.counts.frameworksWithZeroNodes})`, + ...(report.frameworksWithZeroNodes.length + ? report.frameworksWithZeroNodes.map((id) => `- ${id}`) + : ['- none']), + '', + `Related frameworks with no shared node (${report.counts.relatedUnshared})`, + ...(report.relatedUnshared.length + ? report.relatedUnshared.map((item) => `- ${item.page} -> ${item.neighbor}`) + : ['- none']), + '', + ] + return lines.join('\n') +} + + + module.exports = { SCHEMA_VERSION, ValidationError, @@ -987,4 +1151,7 @@ module.exports = { hrefExists, edgeId, TYPE_PREFIX, + coverageReport, + formatCoverage, } + diff --git a/utils/security-map.test.cjs b/utils/security-map.test.cjs index 0c97c13d..d5311e3d 100644 --- a/utils/security-map.test.cjs +++ b/utils/security-map.test.cjs @@ -8,8 +8,11 @@ const { loadAndValidate, generate, serializeGraph, + coverageReport, + formatCoverage, } = require('./security-map-lib.cjs') + const ROOT = path.join(__dirname, '..') function tmpDir(name) { @@ -141,7 +144,7 @@ describe('production catalogue', () => { assert.equal(first.graph.license, 'CC-BY-SA-4.0') assert.doesNotMatch(serializeGraph(first.graph), /\/home\/|generatedAt|C:\\/) const threats = first.graph.nodes.filter((n) => n.type === 'threat') - assert.equal(threats.length, 12) + assert.equal(threats.length, 15) for (const threat of threats) { assert.equal(threat.assessmentEligible, undefined) const hasTarget = first.graph.edges.some((e) => e.type === 'targets' && e.source === threat.id) @@ -154,9 +157,19 @@ describe('production catalogue', () => { assert.ok(hasPath, `${threat.id} missing mitigation/guidance`) } assert.equal(first.graph.nodes.filter((n) => n.type === 'incident').length, 0) + assert.ok(first.graph.nodes.some((n) => n.id === 'component-hardware-wallet')) + assert.ok( + first.graph.edges.some( + (e) => + e.source === 'control-signer-isolation' && + e.target === 'guidance-cold-vs-hot' && + e.type === 'documented-by', + ), + ) }) }) + describe('minimal graph', () => { it('accepts a valid graph', () => { const dir = tmpDir('ok') @@ -488,3 +501,135 @@ describe('synthetic larger fixture', () => { assert.equal(graph.edges.length, 320) }) }) + +describe('coverage report', () => { + it('lists missing controls, guidance, empty frameworks, and unshared related pages', () => { + const dir = tmpDir('cov') + copyProductionTaxonomy(dir) + writeJson(path.join(dir, 'nodes', 'core.json'), { + nodes: [ + { + id: 'asset-funds', + type: 'asset', + title: 'Funds', + summary: 'Value at risk.', + domains: ['governance-treasury'], + status: 'proposed', + tags: ['funds'], + framework: 'alpha', + }, + { + id: 'component-wallet', + type: 'component', + title: 'Wallet', + summary: 'Holds funds.', + domains: ['governance-treasury'], + status: 'proposed', + tags: ['wallet'], + framework: 'alpha', + }, + { + id: 'surface-signing', + type: 'attack-surface', + title: 'Signing', + summary: 'How transactions are signed.', + domains: ['governance-treasury'], + status: 'proposed', + tags: ['signing'], + framework: 'alpha', + }, + { + id: 'threat-key-theft', + type: 'threat', + title: 'Key theft', + summary: 'An attacker steals a signing key.', + domains: ['governance-treasury'], + status: 'proposed', + tags: ['keys'], + severity: 'high', + severityBasis: 'Stolen key is authorized theft.', + framework: 'alpha', + }, + { + id: 'control-hardware-wallet', + type: 'control', + title: 'Hardware wallet', + summary: 'Keys stay on dedicated hardware.', + domains: ['governance-treasury'], + status: 'proposed', + tags: ['hardware'], + controlClass: 'preventive', + assessmentEligible: true, + framework: 'alpha', + }, + { + id: 'guidance-wallet', + type: 'guidance', + title: 'Wallet page', + summary: 'Docs for the wallet control.', + domains: ['governance-treasury'], + status: 'proposed', + tags: ['docs'], + href: '/intro/attack-surface', + framework: 'alpha', + }, + ], + }) + writeJson(path.join(dir, 'edges', 'core.json'), { + edges: [ + { source: 'component-wallet', target: 'surface-signing', type: 'exposes' }, + { source: 'threat-key-theft', target: 'surface-signing', type: 'targets' }, + { source: 'control-hardware-wallet', target: 'threat-key-theft', type: 'mitigates' }, + ], + }) + const pages = tmpDir('cov-pages') + fs.mkdirSync(path.join(pages, 'alpha'), { recursive: true }) + fs.mkdirSync(path.join(pages, 'beta'), { recursive: true }) + fs.writeFileSync( + path.join(pages, 'alpha', 'overview.mdx'), + `---\ntitle: Alpha\n---\n\n# Alpha\n\n## Related frameworks\n\n- [Beta](/beta/overview): neighbor with no shared node\n`, + ) + fs.writeFileSync(path.join(pages, 'beta', 'overview.mdx'), `---\ntitle: Beta\n---\n\n# Beta\n`) + const { graph } = loadAndValidate({ dataDir: dir, pagesDir: pagesDir(), skipMdx: true }) + + const report = coverageReport({ nodes: graph.nodes, edges: graph.edges, pagesDir: pages }) + assert.deepEqual(report.threatsWithoutControl, []) + assert.deepEqual(report.controlsWithoutGuidance, ['control-hardware-wallet']) + assert.deepEqual(report.frameworksWithZeroNodes, ['beta']) + assert.equal(report.relatedUnshared.length, 1) + assert.equal(report.relatedUnshared[0].page, '/alpha/overview') + assert.equal(report.relatedUnshared[0].neighbor, '/beta/overview') + const text = formatCoverage(report) + assert.match(text, /control-hardware-wallet/) + assert.match(text, /\/alpha\/overview -> \/beta\/overview/) + }) + + it('drops related-framework candidates once a typed edge joins the slices', () => { + const dir = tmpDir('cov-shared') + copyProductionTaxonomy(dir) + writeJson(path.join(dir, 'nodes', 'core.json'), { + nodes: [ + ...minimalNodes().map((node) => ({ + ...node, + framework: node.id.startsWith('guidance') ? 'beta' : 'alpha', + })), + ], + }) + writeJson(path.join(dir, 'edges', 'core.json'), { edges: minimalEdges() }) + const pages = tmpDir('cov-shared-pages') + fs.mkdirSync(path.join(pages, 'alpha'), { recursive: true }) + fs.mkdirSync(path.join(pages, 'beta'), { recursive: true }) + fs.writeFileSync( + path.join(pages, 'alpha', 'overview.mdx'), + `---\ntitle: Alpha\n---\n\n# Alpha\n\n## Related frameworks\n\n- [Beta](/beta/overview): now shares documented-by\n`, + ) + fs.writeFileSync(path.join(pages, 'beta', 'overview.mdx'), `---\ntitle: Beta\n---\n\n# Beta\n`) + const { graph } = loadAndValidate({ dataDir: dir, pagesDir: pagesDir(), skipMdx: true }) + + const report = coverageReport({ nodes: graph.nodes, edges: graph.edges, pagesDir: pages }) + assert.deepEqual(report.relatedUnshared, []) + assert.deepEqual(report.frameworksWithZeroNodes, []) + assert.deepEqual(report.controlsWithoutGuidance, []) + }) +}) +