security-mcp 1.1.4 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -264
- package/defaults/checklists/ai.json +20 -1
- package/defaults/checklists/api.json +35 -1
- package/defaults/checklists/infra.json +34 -1
- package/defaults/checklists/mobile.json +23 -1
- package/defaults/checklists/payments.json +15 -1
- package/defaults/checklists/web.json +11 -1
- package/defaults/security-policy.json +2 -2
- package/dist/cli/index.js +0 -0
- package/dist/gate/baseline.js +82 -7
- package/dist/gate/catalog.js +10 -2
- package/dist/gate/checks/ai.js +757 -39
- package/dist/gate/checks/auth-deep.js +920 -216
- package/dist/gate/checks/business-logic.js +751 -0
- package/dist/gate/checks/ci-pipeline.js +399 -4
- package/dist/gate/checks/crypto.js +423 -2
- package/dist/gate/checks/dependencies.js +571 -15
- package/dist/gate/checks/graphql.js +201 -19
- package/dist/gate/checks/infra.js +246 -1
- package/dist/gate/checks/injection-deep.js +827 -184
- package/dist/gate/checks/k8s.js +114 -1
- package/dist/gate/checks/mobile-android.js +917 -3
- package/dist/gate/checks/mobile-ios.js +797 -5
- package/dist/gate/checks/required-artifacts.js +194 -0
- package/dist/gate/checks/runtime.js +178 -0
- package/dist/gate/checks/secrets.js +244 -13
- package/dist/gate/checks/supply-chain-deep.js +787 -0
- package/dist/gate/checks/web-nextjs.js +572 -48
- package/dist/gate/diff.js +17 -5
- package/dist/gate/evidence.js +8 -1
- package/dist/gate/exceptions.js +131 -9
- package/dist/gate/policy.js +280 -131
- package/dist/mcp/audit-chain.js +122 -28
- package/dist/mcp/auth.js +169 -0
- package/dist/mcp/learning.js +129 -4
- package/dist/mcp/model-router.js +158 -21
- package/dist/mcp/orchestration.js +186 -51
- package/dist/mcp/server.js +337 -53
- package/dist/repo/fs.js +24 -1
- package/dist/repo/search.js +31 -6
- package/dist/review/store.js +52 -1
- package/package.json +7 -7
- package/skills/_TEMPLATE/SKILL.md +99 -0
- package/skills/advanced-dos-tester/SKILL.md +109 -0
- package/skills/agentic-loop-exploiter/SKILL.md +368 -0
- package/skills/ai-llm-redteam/SKILL.md +104 -0
- package/skills/ai-model-supply-chain-agent/SKILL.md +103 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +98 -0
- package/skills/android-penetration-tester/SKILL.md +455 -46
- package/skills/anti-replay-tester/SKILL.md +106 -0
- package/skills/appsec-code-auditor/SKILL.md +85 -0
- package/skills/artifact-integrity-analyst/SKILL.md +441 -0
- package/skills/attack-navigator/SKILL.md +467 -8
- package/skills/auth-session-hacker/SKILL.md +102 -0
- package/skills/aws-penetration-tester/SKILL.md +456 -0
- package/skills/azure-penetration-tester/SKILL.md +490 -3
- package/skills/binary-auth-validator/SKILL.md +111 -0
- package/skills/bot-detection-specialist/SKILL.md +109 -0
- package/skills/business-logic-attacker/SKILL.md +231 -0
- package/skills/capec-code-mapper/SKILL.md +84 -0
- package/skills/cert-pin-rotation-specialist/SKILL.md +112 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +405 -0
- package/skills/ciso-orchestrator/SKILL.md +454 -43
- package/skills/cloud-infra-specialist/SKILL.md +118 -0
- package/skills/compliance-gap-analyst/SKILL.md +422 -0
- package/skills/compliance-grc/SKILL.md +85 -0
- package/skills/compliance-lifecycle-tracker/SKILL.md +84 -0
- package/skills/credential-stuffing-specialist/SKILL.md +102 -0
- package/skills/crypto-pki-specialist/SKILL.md +87 -0
- package/skills/csa-ccm-mapper/SKILL.md +84 -0
- package/skills/csf2-governance-mapper/SKILL.md +84 -0
- package/skills/deep-link-fuzzer/SKILL.md +109 -0
- package/skills/dependency-confusion-attacker/SKILL.md +415 -0
- package/skills/device-integrity-aggregator/SKILL.md +108 -0
- package/skills/dos-resilience-tester/SKILL.md +97 -0
- package/skills/dread-scorer/SKILL.md +84 -0
- package/skills/egress-policy-enforcer/SKILL.md +99 -0
- package/skills/evidence-collector/SKILL.md +98 -0
- package/skills/file-upload-attacker/SKILL.md +109 -0
- package/skills/gcp-penetration-tester/SKILL.md +459 -2
- package/skills/git-history-secret-scanner/SKILL.md +106 -0
- package/skills/iam-privesc-graph-builder/SKILL.md +152 -0
- package/skills/incident-responder/SKILL.md +111 -0
- package/skills/injection-specialist/SKILL.md +102 -0
- package/skills/ios-security-auditor/SKILL.md +282 -0
- package/skills/json-ambiguity-tester/SKILL.md +0 -0
- package/skills/k8s-container-escaper/SKILL.md +384 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +98 -0
- package/skills/kill-switch-engineer/SKILL.md +102 -0
- package/skills/linddun-privacy-analyst/SKILL.md +102 -0
- package/skills/logic-race-fuzzer/SKILL.md +443 -0
- package/skills/mobile-api-network-attacker/SKILL.md +421 -0
- package/skills/mobile-binary-hardener/SKILL.md +102 -0
- package/skills/mobile-security-specialist/SKILL.md +85 -0
- package/skills/mobile-webview-auditor/SKILL.md +96 -0
- package/skills/model-extraction-attacker/SKILL.md +219 -0
- package/skills/multipart-abuse-tester/SKILL.md +84 -0
- package/skills/oauth-pkce-specialist/SKILL.md +104 -0
- package/skills/parser-exhaustion-tester/SKILL.md +142 -0
- package/skills/pentest-infra/SKILL.md +98 -0
- package/skills/pentest-social/SKILL.md +201 -0
- package/skills/pentest-team/SKILL.md +87 -0
- package/skills/pentest-web-api/SKILL.md +98 -0
- package/skills/privacy-flow-analyst/SKILL.md +234 -0
- package/skills/prompt-injection-specialist/SKILL.md +394 -0
- package/skills/quantum-migration-planner/SKILL.md +96 -0
- package/skills/rag-poisoning-specialist/SKILL.md +358 -0
- package/skills/registry-mirror-enforcer/SKILL.md +84 -0
- package/skills/rotation-validation-agent/SKILL.md +112 -0
- package/skills/samm-assessor/SKILL.md +85 -0
- package/skills/secrets-mask-bypass-tester/SKILL.md +100 -0
- package/skills/senior-security-engineer/SKILL.md +167 -0
- package/skills/serialization-memory-attacker/SKILL.md +332 -0
- package/skills/session-timeout-tester/SKILL.md +161 -0
- package/skills/slsa-level3-enforcer/SKILL.md +112 -0
- package/skills/slsa-provenance-enforcer/SKILL.md +102 -0
- package/skills/ssrf-detection-validator/SKILL.md +108 -0
- package/skills/step-up-auth-enforcer/SKILL.md +84 -0
- package/skills/stride-pasta-analyst/SKILL.md +420 -0
- package/skills/supply-chain-devsecops/SKILL.md +98 -0
- package/skills/threat-infrastructure-analyst/SKILL.md +84 -0
- package/skills/threat-modeler/SKILL.md +85 -0
- package/skills/tls-certificate-auditor/SKILL.md +573 -18
- package/skills/token-reuse-detector/SKILL.md +95 -0
- package/skills/trike-risk-modeler/SKILL.md +84 -0
- package/skills/unicode-homograph-tester/SKILL.md +84 -0
- package/skills/waf-rule-lifecycle-agent/SKILL.md +97 -0
- package/skills/webhook-security-tester/SKILL.md +102 -0
- package/skills/zero-trust-architect/SKILL.md +109 -0
|
@@ -20,6 +20,28 @@
|
|
|
20
20
|
{ "id": "mobile_threat_model", "description": "Threat model completed and reviewed for this mobile surface change", "critical": true },
|
|
21
21
|
{ "id": "mobile_data_residency", "description": "Data residency requirements met — no user data stored on device beyond session", "critical": false },
|
|
22
22
|
{ "id": "mobile_backup_prevention", "description": "allowBackup=false in Android manifest — sensitive data not included in backups", "critical": true },
|
|
23
|
-
{ "id": "mobile_logging", "description": "No sensitive data logged in production builds — crash reporting sanitized", "critical": true }
|
|
23
|
+
{ "id": "mobile_logging", "description": "No sensitive data logged in production builds — crash reporting sanitized", "critical": true },
|
|
24
|
+
{ "id": "mobile_tapjacking_prevention", "description": "Android tapjacking prevention: FLAG_SECURE set on sensitive screens; filterTouchesWhenObscured=true on clickable elements handling auth or payments", "critical": true },
|
|
25
|
+
{ "id": "mobile_memory_zeroing", "description": "Sensitive data (passwords, keys, PAN) explicitly zeroed from memory after use — not relying on GC; no sensitive data in String objects (use char[] or SecureString)", "critical": false },
|
|
26
|
+
{ "id": "mobile_anti_debugging", "description": "Anti-debugging and anti-tampering controls in place for high-risk operations; ptrace detection and integrity attestation verified before sensitive operations", "critical": false },
|
|
27
|
+
{ "id": "mobile_ios_temp_sensitive", "title": "iOS: Sensitive data not written to NSTemporaryDirectory or NSCachesDirectory without explicit cleanup", "severity": "high", "automated": true },
|
|
28
|
+
{ "id": "mobile_ios_file_protection_none", "title": "iOS: NSFileProtectionNone not set on any file containing sensitive data", "severity": "critical", "automated": true },
|
|
29
|
+
{ "id": "mobile_ios_appstorage_sensitive", "title": "iOS: @AppStorage not used for sensitive credentials or tokens", "severity": "high", "automated": true },
|
|
30
|
+
{ "id": "mobile_ios_sqlite_unencrypted", "title": "iOS: SQLite databases encrypted with SQLCipher when storing sensitive data", "severity": "high", "automated": true },
|
|
31
|
+
{ "id": "mobile_ios_webview_http", "title": "iOS: WKWebView does not load http:// URLs when JavaScript is enabled", "severity": "critical", "automated": true },
|
|
32
|
+
{ "id": "mobile_ios_universal_links", "title": "iOS: Universal Links configured with HTTPS AASA and restrictive path patterns", "severity": "high", "automated": true },
|
|
33
|
+
{ "id": "mobile_android_root_detection", "title": "Android: Root detection implemented for high-risk operations", "severity": "medium", "automated": true },
|
|
34
|
+
{ "id": "mobile_android_frida_magisk_detection", "title": "Android: Frida/Magisk/Xposed detection in place for high-risk app flows", "severity": "medium", "automated": true },
|
|
35
|
+
{ "id": "mobile_android_webview_ssl_error", "title": "Android: WebViewClient.onReceivedSslError does not call proceed() unconditionally", "severity": "critical", "automated": true },
|
|
36
|
+
{ "id": "mobile_android_firebase_rules", "title": "Android: Firebase Realtime Database and Firestore rules deny unauthenticated access", "severity": "critical", "automated": true },
|
|
37
|
+
{ "id": "mobile_android_maps_api_key", "title": "Android: Google Maps API key not hardcoded in manifest or resource files", "severity": "high", "automated": true },
|
|
38
|
+
{ "id": "mobile_android_deeplink_traversal", "title": "Android: Deep link path parameters validated and sanitized before use in file or URL operations", "severity": "high", "automated": true },
|
|
39
|
+
{ "id": "mobile_android_sharedprefs_world", "title": "Android: SharedPreferences not opened with MODE_WORLD_READABLE or MODE_WORLD_WRITEABLE", "severity": "critical", "automated": true },
|
|
40
|
+
{ "id": "mobile_android_content_provider_permission", "title": "Android: ContentProvider with exported=true has explicit readPermission and writePermission", "severity": "high", "automated": true },
|
|
41
|
+
{ "id": "mobile_rn_async_storage_sensitive", "title": "React Native: AsyncStorage not used for credentials or tokens — use react-native-keychain or Expo SecureStore", "severity": "high", "automated": true },
|
|
42
|
+
{ "id": "mobile_rn_codepush_integrity", "title": "React Native: OTA bundle updates verified with code signing before execution", "severity": "high", "automated": true },
|
|
43
|
+
{ "id": "mobile_flutter_sharedprefs_sensitive", "title": "Flutter: Sensitive data stored in flutter_secure_storage, not shared_preferences", "severity": "high", "automated": true },
|
|
44
|
+
{ "id": "mobile_expo_async_vs_secure", "title": "Expo: Credentials and tokens stored in SecureStore, not AsyncStorage", "severity": "high", "automated": true },
|
|
45
|
+
{ "id": "mobile_certificate_transparency", "title": "Certificate Transparency enforcement enabled for production domains", "severity": "medium", "automated": true }
|
|
24
46
|
]
|
|
25
47
|
}
|
|
@@ -20,6 +20,20 @@
|
|
|
20
20
|
{ "id": "pci_chargeback_monitoring", "description": "Chargeback monitoring and alerting configured with defined response process", "critical": false },
|
|
21
21
|
{ "id": "pci_data_retention", "description": "Payment data retention policy enforced — data purged per PCI DSS schedule", "critical": true },
|
|
22
22
|
{ "id": "pci_ir_playbook", "description": "Payment fraud and PCI breach IR playbooks exist and are current", "critical": true },
|
|
23
|
-
{ "id": "pci_threat_model", "description": "Threat model completed and reviewed for this payment surface change", "critical": true }
|
|
23
|
+
{ "id": "pci_threat_model", "description": "Threat model completed and reviewed for this payment surface change", "critical": true },
|
|
24
|
+
{ "id": "pci_magecart_prevention", "description": "Magecart/digital-skimming prevention: SRI hashes on all checkout page scripts; CSP blocks unauthorized exfiltration destinations; DOM mutation monitoring detects injected form skimmers", "critical": true },
|
|
25
|
+
{ "id": "pci_3ds_enforced", "description": "EMV 3D Secure 2.2+ enforced for card-not-present transactions above risk threshold; step-up authentication triggered by anomaly score; 3DS bypass attempts logged and alerted", "critical": true },
|
|
26
|
+
{ "id": "pci_currency_validation", "title": "Payment currency fixed server-side — client-supplied currency code rejected or validated against strict allowlist", "severity": "critical", "automated": true },
|
|
27
|
+
{ "id": "pci_discount_stacking", "title": "Discount/coupon stacking limited server-side — maximum one promotion per order enforced atomically", "severity": "high", "automated": true },
|
|
28
|
+
{ "id": "pci_payment_confirmation_server_side", "title": "Order/fulfillment status derived exclusively from payment processor API response — never from client-supplied status field", "severity": "critical", "automated": true },
|
|
29
|
+
{ "id": "pci_webhook_timestamp_tolerance", "title": "Webhook timestamp tolerance ≤ 300 seconds — stale events rejected", "severity": "high", "automated": true },
|
|
30
|
+
{ "id": "pci_server_side_totals", "title": "Tax, shipping, and discount amounts computed server-side — never sourced from client request body", "severity": "high", "automated": true },
|
|
31
|
+
{ "id": "pci_total_revalidation", "title": "Final charge amount re-computed server-side from canonical item prices — client-supplied total field never used as charge amount", "severity": "critical", "automated": true },
|
|
32
|
+
{ "id": "pci_referral_abuse_prevention", "title": "Referral/signup bonus protected against self-referral and multi-account abuse with deduplication", "severity": "high", "automated": true },
|
|
33
|
+
{ "id": "biz_email_normalization", "title": "Email addresses normalised before uniqueness check — duplicate account creation via aliasing prevented", "severity": "high", "automated": true },
|
|
34
|
+
{ "id": "biz_feature_flag_server_side", "title": "Feature entitlements derived from server-side session/database record — not from client-supplied plan/tier/featureFlag parameters", "severity": "high", "automated": true },
|
|
35
|
+
{ "id": "biz_api_version_controls_parity", "title": "All security controls applied uniformly across all live API versions — deprecated versions sunset or mirrored", "severity": "high", "automated": false },
|
|
36
|
+
{ "id": "pci_trial_abuse_prevention", "title": "Free trial creation checked for velocity on payment method fingerprint, BIN prefix, IP, and device identifier", "severity": "high", "automated": true },
|
|
37
|
+
{ "id": "pci_payment_intent_idempotency", "title": "Payment intent charge guarded by distributed lock or unique constraint — concurrent double-charge prevented", "severity": "critical", "automated": true }
|
|
24
38
|
]
|
|
25
39
|
}
|
|
@@ -25,6 +25,16 @@
|
|
|
25
25
|
{ "id": "web_sast_pass", "description": "SAST scan passed with no CRITICAL findings", "critical": true },
|
|
26
26
|
{ "id": "web_secrets_scan", "description": "Secrets scan clean — no credentials or tokens in source code", "critical": true },
|
|
27
27
|
{ "id": "web_logging", "description": "Required security events logged — no PII, tokens, or secrets in logs", "critical": false },
|
|
28
|
-
{ "id": "web_staging_verified", "description": "Security headers verified in staging environment with automated check", "critical": false }
|
|
28
|
+
{ "id": "web_staging_verified", "description": "Security headers verified in staging environment with automated check", "critical": false },
|
|
29
|
+
{ "id": "web_dom_clobbering", "description": "DOM clobbering prevention reviewed — named form inputs, anchors, and embeds do not shadow window/document properties used in application logic", "critical": true },
|
|
30
|
+
{ "id": "web_cache_poisoning", "description": "Web cache poisoning prevented — unkeyed headers (X-Forwarded-Host, X-Original-URL, X-Rewrite-URL) are not reflected in cached responses or redirects", "critical": true },
|
|
31
|
+
{ "id": "web_websocket_auth", "description": "WebSocket upgrade validates authentication independently (token in first message or query param signed); does not rely solely on session cookies which bypass CORS", "critical": true },
|
|
32
|
+
{ "id": "web_postmessage_origin", "description": "postMessage listeners validate event.origin against an explicit allowlist before processing any message data — no wildcard '*' as trusted origin", "critical": true },
|
|
33
|
+
{ "id": "web_dangling_markup", "description": "Dangling markup injection reviewed — no user-controlled partial HTML tags output across multi-step page generation; prevents attribute-injection data exfiltration", "critical": false },
|
|
34
|
+
{ "id": "web_css_injection", "title": "User input not written into CSS style attributes, CSS-in-JS template literals, or <style> blocks", "severity": "high", "automated": true },
|
|
35
|
+
{ "id": "web_dangling_markup_check", "title": "Dangling markup injection prevention — no user-controlled partial HTML tags in server-rendered output", "severity": "high", "automated": true },
|
|
36
|
+
{ "id": "web_postmessage_wildcard", "title": "postMessage sender does not use wildcard '*' as targetOrigin for sensitive data", "severity": "medium", "automated": true },
|
|
37
|
+
{ "id": "web_cache_poisoning_headers", "title": "Unkeyed headers (X-Forwarded-Host, X-Original-URL) not reflected in cached responses or redirects", "severity": "medium", "automated": true },
|
|
38
|
+
{ "id": "web_missing_sri", "title": "All external scripts and stylesheets loaded with Subresource Integrity (SRI) integrity= attribute", "severity": "medium", "automated": true }
|
|
29
39
|
]
|
|
30
40
|
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"description": "Default security gate policy for security-mcp. Copy to .mcp/policies/security-policy.json and customize for your project.",
|
|
5
5
|
"required_checks": {
|
|
6
6
|
"secrets_scan": { "severity_block": ["HIGH", "CRITICAL"] },
|
|
7
|
-
"dependency_scan": { "severity_block": ["CRITICAL"] },
|
|
8
|
-
"sast": { "severity_block": ["CRITICAL"] },
|
|
7
|
+
"dependency_scan": { "severity_block": ["HIGH", "CRITICAL"] },
|
|
8
|
+
"sast": { "severity_block": ["HIGH", "CRITICAL"] },
|
|
9
9
|
"iac_scan": { "severity_block": ["HIGH", "CRITICAL"] }
|
|
10
10
|
},
|
|
11
11
|
"environments": {
|
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/gate/baseline.js
CHANGED
|
@@ -5,7 +5,39 @@
|
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
8
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
8
9
|
import { join } from "node:path";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// HMAC integrity helpers — TM-013 fix
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// HMAC-SHA256 requires at least 32 bytes (256 bits) per NIST SP 800-107 §5.3.4.
|
|
14
|
+
const HMAC_MIN_KEY_BYTES = 32;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the HMAC key from env, or null if not configured.
|
|
17
|
+
* Throws if the key is present but too short.
|
|
18
|
+
*/
|
|
19
|
+
function getHmacKey() {
|
|
20
|
+
const key = process.env["SECURITY_POLICY_HMAC_KEY"];
|
|
21
|
+
if (!key)
|
|
22
|
+
return null;
|
|
23
|
+
if (Buffer.byteLength(key, "utf-8") < HMAC_MIN_KEY_BYTES) {
|
|
24
|
+
throw new Error(`SECURITY_POLICY_HMAC_KEY is too short (${Buffer.byteLength(key, "utf-8")} bytes). ` +
|
|
25
|
+
`Provide at least ${HMAC_MIN_KEY_BYTES} bytes — generate one with: ` +
|
|
26
|
+
`node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`);
|
|
27
|
+
}
|
|
28
|
+
return key;
|
|
29
|
+
}
|
|
30
|
+
function signBaseline(json, key) {
|
|
31
|
+
return createHmac("sha256", key).update(json, "utf-8").digest("hex");
|
|
32
|
+
}
|
|
33
|
+
function verifyBaselineHmac(json, stored, key) {
|
|
34
|
+
const expected = createHmac("sha256", key).update(json, "utf-8").digest("hex");
|
|
35
|
+
const storedBuf = Buffer.from(stored, "hex");
|
|
36
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
37
|
+
if (storedBuf.length !== expectedBuf.length)
|
|
38
|
+
return false;
|
|
39
|
+
return timingSafeEqual(storedBuf, expectedBuf);
|
|
40
|
+
}
|
|
9
41
|
const execFileAsync = promisify(execFile);
|
|
10
42
|
const BASELINE_DIR = join(process.cwd(), ".mcp", "baselines");
|
|
11
43
|
async function ensureDir(dir) {
|
|
@@ -32,37 +64,53 @@ export async function getCommitHash() {
|
|
|
32
64
|
/**
|
|
33
65
|
* Saves a gate result as baseline for the given commit hash.
|
|
34
66
|
* Also updates the latest baseline copy.
|
|
67
|
+
*
|
|
68
|
+
* TM-013 fix: When SECURITY_POLICY_HMAC_KEY is set, the serialised payload is
|
|
69
|
+
* HMAC-SHA256 signed and the signature is stored in the envelope. Unsigned
|
|
70
|
+
* writes are still permitted when no key is configured (graceful degradation),
|
|
71
|
+
* but loadBaseline will reject a previously-signed file whose signature no
|
|
72
|
+
* longer matches (tamper detection).
|
|
35
73
|
*/
|
|
36
74
|
export async function saveBaseline(runId, result, commitHash) {
|
|
37
75
|
await ensureDir(BASELINE_DIR);
|
|
38
76
|
const payload = { runId, commitHash, savedAt: new Date().toISOString(), result };
|
|
39
77
|
const json = JSON.stringify(payload, null, 2);
|
|
78
|
+
// Sign if a key is available
|
|
79
|
+
const hmacKey = getHmacKey();
|
|
80
|
+
const envelope = hmacKey
|
|
81
|
+
? JSON.stringify({ payload, hmacSha256: signBaseline(json, hmacKey) }, null, 2)
|
|
82
|
+
: json;
|
|
40
83
|
// Write to temp file then rename (atomic)
|
|
41
84
|
const safehash = commitHash.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
|
42
85
|
const targetPath = join(BASELINE_DIR, `${safehash}.json`);
|
|
43
86
|
const latestPath = join(BASELINE_DIR, "latest.json");
|
|
44
|
-
const tmpPath = `${targetPath}.tmp`;
|
|
87
|
+
const tmpPath = `${targetPath}.${randomBytes(8).toString("hex")}.tmp`;
|
|
45
88
|
try {
|
|
46
|
-
await writeFile(tmpPath,
|
|
89
|
+
await writeFile(tmpPath, envelope, "utf-8");
|
|
47
90
|
await rename(tmpPath, targetPath);
|
|
48
91
|
}
|
|
49
92
|
catch {
|
|
50
93
|
// fallback: write directly
|
|
51
|
-
await writeFile(targetPath,
|
|
94
|
+
await writeFile(targetPath, envelope, "utf-8").catch(() => { });
|
|
52
95
|
}
|
|
53
96
|
// Update latest (best-effort atomic)
|
|
54
|
-
const latestTmp = `${latestPath}.tmp`;
|
|
97
|
+
const latestTmp = `${latestPath}.${randomBytes(8).toString("hex")}.tmp`;
|
|
55
98
|
try {
|
|
56
|
-
await writeFile(latestTmp,
|
|
99
|
+
await writeFile(latestTmp, envelope, "utf-8");
|
|
57
100
|
await rename(latestTmp, latestPath);
|
|
58
101
|
}
|
|
59
102
|
catch {
|
|
60
|
-
await writeFile(latestPath,
|
|
103
|
+
await writeFile(latestPath, envelope, "utf-8").catch(() => { });
|
|
61
104
|
}
|
|
62
105
|
}
|
|
63
106
|
/**
|
|
64
107
|
* Loads a baseline by commit hash, or the latest baseline if no hash given.
|
|
65
108
|
* Returns null if no baseline exists or it's corrupted.
|
|
109
|
+
*
|
|
110
|
+
* TM-013 fix: If the file is stored in the HMAC envelope format AND
|
|
111
|
+
* SECURITY_POLICY_HMAC_KEY is configured, the HMAC is verified before the
|
|
112
|
+
* payload is returned. A tampered baseline (missing or wrong HMAC) is
|
|
113
|
+
* rejected — the gate will run without a baseline rather than trust forged data.
|
|
66
114
|
*/
|
|
67
115
|
export async function loadBaseline(commitHash) {
|
|
68
116
|
await ensureDir(BASELINE_DIR);
|
|
@@ -76,7 +124,34 @@ export async function loadBaseline(commitHash) {
|
|
|
76
124
|
}
|
|
77
125
|
try {
|
|
78
126
|
const raw = await readFile(filePath, "utf-8");
|
|
79
|
-
const
|
|
127
|
+
const top = JSON.parse(raw);
|
|
128
|
+
// Detect envelope format (has both "payload" and "hmacSha256")
|
|
129
|
+
if ("payload" in top && "hmacSha256" in top) {
|
|
130
|
+
const envelope = top;
|
|
131
|
+
const hmacKey = getHmacKey();
|
|
132
|
+
if (hmacKey) {
|
|
133
|
+
// Re-serialise the inner payload the same way saveBaseline did
|
|
134
|
+
const expectedInput = JSON.stringify(envelope.payload, null, 2);
|
|
135
|
+
if (!verifyBaselineHmac(expectedInput, envelope.hmacSha256, hmacKey)) {
|
|
136
|
+
console.error("[baseline] HMAC verification failed — baseline may have been tampered. Ignoring.");
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Key not configured: we can't verify, but we can warn
|
|
142
|
+
console.warn("[baseline] Baseline is signed but SECURITY_POLICY_HMAC_KEY is not set — skipping HMAC verification.");
|
|
143
|
+
}
|
|
144
|
+
return envelope.payload.result ?? null;
|
|
145
|
+
}
|
|
146
|
+
// Legacy format (unsigned) — parse directly
|
|
147
|
+
const parsed = top;
|
|
148
|
+
const hmacKey = getHmacKey();
|
|
149
|
+
if (hmacKey) {
|
|
150
|
+
// A key is configured but the file is unsigned — reject it to prevent
|
|
151
|
+
// an attacker from stripping the HMAC wrapper to bypass verification.
|
|
152
|
+
console.error("[baseline] SECURITY_POLICY_HMAC_KEY is set but baseline is unsigned — ignoring to prevent tampering bypass.");
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
80
155
|
return parsed.result ?? null;
|
|
81
156
|
}
|
|
82
157
|
catch {
|
package/dist/gate/catalog.js
CHANGED
|
@@ -23,8 +23,16 @@ async function readJsonWithFallback(relPath, fallbackName) {
|
|
|
23
23
|
".mcp/catalog/control-catalog.json": "SECURITY_GATE_CONTROL_CATALOG"
|
|
24
24
|
};
|
|
25
25
|
const overrideEnv = overrideEnvMap[relPath];
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const overridePath = overrideEnv ? process.env[overrideEnv] : undefined;
|
|
27
|
+
if (overridePath) {
|
|
28
|
+
// Guard against path traversal (VULN-003 / CWE-22): resolve() + startsWith() is required;
|
|
29
|
+
// join() alone normalises '..' but does not prevent escape from the project directory.
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
const resolved = resolve(cwd, overridePath);
|
|
32
|
+
if (resolved !== cwd && !resolved.startsWith(cwd + "/")) {
|
|
33
|
+
throw new Error(`${overrideEnv} path escapes the project directory`);
|
|
34
|
+
}
|
|
35
|
+
return await readFile(resolved, "utf-8");
|
|
28
36
|
}
|
|
29
37
|
try {
|
|
30
38
|
return await readFile(join(process.cwd(), relPath), "utf-8");
|