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
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@ GoDaddy CLI is a Rust binary (edition 2024) built using:

### Extension security scanner

- Post-bundle regex scanner in `extension/security.rs`.
- Rules SEC101–SEC115; uses `fancy-regex` for lookahead support.
- Post-bundle regex scanner in `extension/security/mod.rs` (rule data in
`extension/security/rules.rs`).
- 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
13 changes: 8 additions & 5 deletions rust/src/extension/security/rules.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! SEC101–SEC115 rule data.
//! Ported from src/core/security/rules/bundle/ in the TypeScript CLI.
//! SEC101–SEC110 ported from src/core/security/rules/bundle/ in the TypeScript
//! CLI. SEC111–SEC115 were added during the Rust port and have no TS
//! baseline (SEC111/SEC112/SEC115 block, SEC113/SEC114 warn — see the
//! comments above each for why).

use crate::extension::types::Severity;

Expand Down Expand Up @@ -231,21 +234,21 @@ pub(super) 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",
signal_patterns: &[],
patterns: &[r#"\bdebugger\b"#],
Expand Down
114 changes: 105 additions & 9 deletions rust/src/extension/security/tests_sec109_115.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ fn sec109_large_base64_buffer_from() {
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 @@ -301,21 +301,21 @@ fn sec112_godaddy_lookalike_blocked() {
}

// -----------------------------------------------------------------------
// 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 @@ -327,7 +327,7 @@ fn sec113_buffer_from_base64_blocked() {
}

#[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 @@ -348,17 +348,17 @@ fn sec113_buffer_from_utf8_allowed() {
}

// -----------------------------------------------------------------------
// 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 @@ -419,3 +419,99 @@ fn sec115_import_meta_not_matched() {
"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:?}"
);
}
Loading