Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Finding>`, `is_blocked(findings) -> bool`.

### esbuild dependency
Expand Down
131 changes: 118 additions & 13 deletions rust/src/extension/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -332,21 +341,21 @@ 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: &[
r#"\batob\s*\("#,
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",
Comment on lines +358 to 359
signal_patterns: &[],
patterns: &[r#"\bdebugger\b"#],
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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",
Expand All @@ -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"),
Expand All @@ -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]
Expand Down Expand Up @@ -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:?}"
);
}
Comment on lines +2805 to +2817
}
Loading