diff --git a/AGENTS.md b/AGENTS.md index 569d91a..90d8259 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,7 +66,9 @@ GoDaddy CLI is a Rust binary (edition 2024) built using: ### Extension security scanner - Post-bundle regex scanner in `extension/mod.rs`. -- Rules SEC101–SEC115; uses `fancy-regex` for lookahead support. +- Rules SEC101–SEC110 ported from the TS scanner; SEC111–SEC115 added in the + Rust port with no TS baseline (SEC111/SEC112/SEC115 block, SEC113/SEC114 + warn). Uses `fancy-regex` for lookahead support. - `scan_bundle(content, path) -> Vec`, `is_blocked(findings) -> bool`. ### esbuild dependency diff --git a/rust/src/extension/mod.rs b/rust/src/extension/mod.rs index 4aa393b..1cf7df5 100644 --- a/rust/src/extension/mod.rs +++ b/rust/src/extension/mod.rs @@ -94,6 +94,9 @@ pub struct Finding { // --------------------------------------------------------------------------- // Bundle security rules (SEC101–SEC110) // Ported from src/core/security/rules/bundle/ in the TypeScript CLI. +// +// SEC111–SEC115 (defined further below) were added during the Rust port and +// have no TS baseline, see the comment above SEC111 for details. // --------------------------------------------------------------------------- struct RuleDef { @@ -301,6 +304,12 @@ static RULE_DEFS: &[RuleDef] = &[ r#"fs\.readFile(?:Sync)?\s*\(\s*['"][^'"]*\.env['"]"#, ], }, + // --------------------------------------------------------------------- + // Bundle security rules (SEC111–SEC115) + // Added in the Rust port; not present in the TS baseline. Unlike + // SEC101–SEC110, SEC112–SEC115 have no signal_patterns (single-pass), + // which is a higher false-positive surface for a Block rule. + // --------------------------------------------------------------------- // SEC111 — destructive fs operations (two-pass, block) RuleDef { id: "SEC111", @@ -332,10 +341,10 @@ static RULE_DEFS: &[RuleDef] = &[ r#"https?://(?!(?:(?:[a-zA-Z0-9-]+\.)*godaddy\.com|localhost|127\.0\.0\.1)(?:[:/?#\s]|$))[^\s"'\x60<>]+"#, ], }, - // SEC113 — any encoded payload (no signal, block) + // SEC113 — any encoded payload (no signal, warn) RuleDef { id: "SEC113", - severity: Severity::Block, + severity: Severity::Warn, description: "Bundled code uses base64/hex encoding that could conceal malicious payloads", signal_patterns: &[], patterns: &[ @@ -343,10 +352,10 @@ static RULE_DEFS: &[RuleDef] = &[ r#"Buffer\.from\s*\(\s*['"][^'"]+['"]\s*,\s*['"](?:base64|hex)['"]"#, ], }, - // SEC114 — debugger statement (no signal, block) + // SEC114 — debugger statement (no signal, warn) RuleDef { id: "SEC114", - severity: Severity::Block, + severity: Severity::Warn, description: "Bundled code contains a debugger statement which enables remote debugging access", signal_patterns: &[], patterns: &[r#"\bdebugger\b"#], @@ -2308,7 +2317,7 @@ export function handler() { return { success: true }; } findings.iter().any(|f| f.rule_id == "SEC109"), "findings: {findings:?}" ); - // SEC113 (block) also fires on base64 content — just verify SEC109 itself is warn. + // SEC113 (warn) also fires on base64 content — just verify SEC109 itself is warn. assert!( findings .iter() @@ -2592,21 +2601,21 @@ export function handler() { return { success: true }; } } // ----------------------------------------------------------------------- - // SEC113 — any encoded payload (no signal, block) + // SEC113 — any encoded payload (no signal, warn) // ----------------------------------------------------------------------- #[test] - fn sec113_atob_blocked() { + fn sec113_atob_warns() { let findings = scan_bundle(r#"const x = atob("aGVsbG8=");"#, "test.mjs"); assert!( findings.iter().any(|f| f.rule_id == "SEC113"), "findings: {findings:?}" ); - assert!(is_blocked(&findings)); + assert!(!is_blocked(&findings), "SEC113 should be warn"); } #[test] - fn sec113_buffer_from_base64_blocked() { + fn sec113_buffer_from_base64_warns() { let findings = scan_bundle( r#"const x = Buffer.from("shortval", "base64");"#, "test.mjs", @@ -2618,7 +2627,7 @@ export function handler() { return { success: true }; } } #[test] - fn sec113_buffer_from_hex_blocked() { + fn sec113_buffer_from_hex_warns() { let findings = scan_bundle(r#"const x = Buffer.from("deadbeef", "hex");"#, "test.mjs"); assert!( findings.iter().any(|f| f.rule_id == "SEC113"), @@ -2639,17 +2648,17 @@ export function handler() { return { success: true }; } } // ----------------------------------------------------------------------- - // SEC114 — debugger statement (no signal, block) + // SEC114 — debugger statement (no signal, warn) // ----------------------------------------------------------------------- #[test] - fn sec114_debugger_blocked() { + fn sec114_debugger_warns() { let findings = scan_bundle("function x() { debugger; return 1; }", "test.mjs"); assert!( findings.iter().any(|f| f.rule_id == "SEC114"), "findings: {findings:?}" ); - assert!(is_blocked(&findings)); + assert!(!is_blocked(&findings), "SEC114 should be warn"); } #[test] @@ -2710,4 +2719,100 @@ export function handler() { return { success: true }; } "import.meta.url should not match SEC115: {findings:?}" ); } + + #[test] + fn sec112_third_party_api_call_still_blocks() { + // A call to a legitimate third-party API (e.g. Stripe) is still + // flagged by SEC112's godaddy.com-only allowlist. This is expected, + // unchanged behavior. + let findings = scan_bundle( + r#"const res = await fetch("https://api.stripe.com/v1/charges");"#, + "test.mjs", + ); + assert!( + findings.iter().any(|f| f.rule_id == "SEC112"), + "findings: {findings:?}" + ); + assert!(is_blocked(&findings)); + } + + #[test] + fn sec113_jwt_decode_warns_not_blocks() { + // jwt-decode-style JWT payload decoding via atob() is a common, + // benign pattern. SEC113 still fires (informational) but no longer + // blocks, per DEVEX-711. + let findings = scan_bundle( + r#"const payload = JSON.parse(atob(token.split(".")[1]));"#, + "test.mjs", + ); + assert!( + findings.iter().any(|f| f.rule_id == "SEC113"), + "findings: {findings:?}" + ); + assert!(!is_blocked(&findings), "SEC113 should be warn"); + } + + #[test] + fn sec113_data_uri_decode_warns_not_blocks() { + // Decoding an inline SVG/image data URI is a common, benign use of + // base64 decoding. + let findings = scan_bundle( + r#"const svg = Buffer.from("PHN2ZyB4bWxucz0i...", "base64").toString("utf8");"#, + "test.mjs", + ); + assert!( + findings.iter().any(|f| f.rule_id == "SEC113"), + "findings: {findings:?}" + ); + assert!(!is_blocked(&findings), "SEC113 should be warn"); + } + + #[test] + fn sec114_debugger_in_realistic_function_warns_not_blocks() { + let findings = scan_bundle( + r#" + function handleClick(event) { + if (process.env.NODE_ENV === "development") { + debugger; + } + return event.target.value; + } + "#, + "test.mjs", + ); + assert!( + findings.iter().any(|f| f.rule_id == "SEC114"), + "findings: {findings:?}" + ); + assert!(!is_blocked(&findings), "SEC114 should be warn"); + } + + #[test] + fn sec115_plugin_loader_by_computed_path_still_blocks() { + // Resolving the module path via a function call + // rather than a literal. SEC115 stays + let findings = scan_bundle( + r#"const plugin = await import(resolvePluginPath(pluginName));"#, + "test.mjs", + ); + assert!( + findings.iter().any(|f| f.rule_id == "SEC115"), + "findings: {findings:?}" + ); + assert!(is_blocked(&findings)); + } + + #[test] + fn sec115_template_literal_require_not_matched() { + // SEC115's regex treats any leading backtick as a "literal" argument, + // so this does not fire today + let findings = scan_bundle( + r#"const plugin = require(`./plugins/${pluginName}`);"#, + "test.mjs", + ); + assert!( + findings.iter().all(|f| f.rule_id != "SEC115"), + "findings: {findings:?}" + ); + } }