Skip to content

fix(extensions): restore pre-bundle AST security scan [DEVEX-710] - #208

Open
qcai-godaddy wants to merge 6 commits into
mainfrom
DEVEX-710
Open

fix(extensions): restore pre-bundle AST security scan [DEVEX-710]#208
qcai-godaddy wants to merge 6 commits into
mainfrom
DEVEX-710

Conversation

@qcai-godaddy

@qcai-godaddy qcai-godaddy commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Restore pre-bundle oxc AST scanning (SEC001–SEC010/SEC012) plus SEC011 package-script scanning under extension/security/, with scan_extension returning Result<ScanReport, ScanError> so discovery/read/parse/symlink failures fail closed.
  • Wire scan.prebundle into deploy before esbuild; block on severity Block, then keep the existing post-bundle regex scan (SEC101–SEC115).
  • SEC012 parity: DOM pair/triple blocklists, scope shadowing, and host mount({ container }) still catching container escapes (closest, ownerDocument, etc.) without treating container as a free-variable shadow.
  • Hardening follow-ups: UTF-8-safe snippet/offset helpers, deterministic finding sort (file/line/col/rule_id), fail-closed file discovery (read_dir / symlink).

Test plan

  • cargo build / cargo clippy -- -D warnings / cargo fmt --check / cargo test (incl. 132 extension::security tests)
  • Manual fixture scan (pre-bundle only):
    • clean / shadow-ok → pass (0 findings)
    • eval-bad → blocked (SEC001)
    • dom-bad → blocked (SEC012, 3 findings)
  • End-to-end gddy platform app deploy on a real app (needs experimental stage + auth + esbuild): confirm pre-bundle blocks bad packages and clean packages proceed past scan to upload

Copilot AI lite review requested due to automatic review settings August 12, 2026 19:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR restores and wires a pre-bundle AST-based security scan for extensions (SEC001–SEC012 plus SEC011 package scripts) so deploy can block unsafe extensions before bundling, while retaining the existing post-bundle regex scan (SEC101–SEC115).

Changes:

  • Adds a pre-bundle scan pipeline (scan_extension) with oxc-based AST rules and a deploy-time block gate before esbuild.
  • Introduces supporting modules for AST scanning (alias mapping, DOM-escape rule SEC012, file discovery, config/util helpers) and expands findings to include column info.
  • Updates deploy output/error formatting for blocked findings and updates docs/dependencies to support the new scanner.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
rust/src/extension/types.rs Extends Finding with message: String and col for AST-location reporting.
rust/src/extension/security/util.rs Adds shared helpers (URL detection, first string arg, line/col + snippet extraction).
rust/src/extension/security/types.rs Adds shared scan types (AliasMaps, ScanReport, summaries).
rust/src/extension/security/source.rs Implements scan_extension orchestration + unit tests for key rules/flows.
rust/src/extension/security/scripts_scanner.rs Adds SEC011 package.json lifecycle script scanning (warn-only).
rust/src/extension/security/mod.rs Wires new modules and re-exports scan_extension; updates bundle findings to use String + col.
rust/src/extension/security/file_discovery.rs Adds recursive source discovery with exclude globs.
rust/src/extension/security/engine.rs Implements AST rule engine for SEC001–SEC010 and hooks SEC012 DOM-escape scan.
rust/src/extension/security/dom_escape.rs Implements SEC012 DOM escape detection with scope-aware shadowing logic.
rust/src/extension/security/config.rs Adds immutable security config (trusted domains + exclude patterns).
rust/src/extension/security/alias_builder.rs Builds module alias maps from imports/requires to detect aliased sensitive APIs.
rust/src/extension/mod.rs Exposes scan_extension from the extension module surface.
rust/src/application/commands/deploy/mod.rs Updates deploy command help text to describe new pre-bundle scanning.
rust/src/application/commands/deploy/extensions.rs Runs scan.prebundle before bundling; standardizes blocked finding formatting.
rust/Cargo.toml Adds dependencies required for AST scanning and file discovery (oxc_*, globset).
rust/Cargo.lock Locks new dependency graph for the added crates.
AGENTS.md Documents the restored pre-bundle scanner and updated deploy scan order.
Suppressed comments (1)

rust/src/extension/security/file_discovery.rs:39

  • Iterating with entries.flatten() (and file_type’s Err(_) => continue) silently drops per-entry IO errors. That can skip files during the scan without surfacing an error, undermining the “fail on discovery/read errors” behavior.
    for entry in entries.flatten() {
        let path = entry.path();
        let path_str = path.to_string_lossy();
        if should_exclude(&path_str, excludes) {
            continue;

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread rust/src/extension/security/file_discovery.rs Outdated
Comment thread rust/src/application/commands/deploy/mod.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.

Suppressed comments (2)

rust/src/extension/security/source.rs:39

  • Finding now includes a column, but the final output ordering in scan_extension only sorts by file+line. When multiple findings share the same line, the order can be unstable/non-deterministic; include col in the sort key for consistent output.
    findings.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));

rust/src/application/commands/deploy/extensions.rs:96

  • format_security_findings always formats locations as file:line when col == 0. SEC011 findings use line = 0/col = 0, so the user-facing message will include package.json:0, which is confusing. Consider omitting the line/col entirely when line == 0.
            let loc = if f.col > 0 {
                format!("{}:{}:{}", f.file, f.line, f.col)
            } else {
                format!("{}:{}", f.file, f.line)
            };

Comment thread rust/src/extension/security/engine.rs Outdated
Comment thread rust/src/extension/security/scripts_scanner.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated no new comments.

Suppressed comments (3)

rust/src/extension/security/util.rs:39

  • snippet_at slices source[start..end] directly. Because Span offsets are byte indices, this can panic if the span is not on UTF-8 char boundaries (or if a span is malformed with start > end). A panic here would crash the security scan instead of failing closed / returning an error.
pub fn snippet_at(source: &str, span: Span) -> String {
    let start = (span.start as usize).min(source.len());
    let end = (span.end as usize).min(source.len());
    let slice = &source[start..end];
    let line = slice.lines().next().unwrap_or(slice);

rust/src/extension/security/source.rs:42

  • Findings are sorted only by file + line. With the new col field (and multiple matches possible on the same line), Vec::sort_by can produce nondeterministic ordering for equal keys, which makes output and tests/flaky debugging harder. Include col (and optionally rule_id) in the sort key for deterministic ordering.
    findings.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));

rust/src/application/commands/deploy/extensions.rs:96

  • Finding.line (and col) are used as sentinels for some scanners (line: 0 in SEC011, col: 0 for regex matches). Formatting these as file:0 reads like a real source location and is confusing. Consider omitting the line/col portion when line == 0.
            let loc = if f.col > 0 {
                format!("{}:{}:{}", f.file, f.line, f.col)
            } else {
                format!("{}:{}", f.file, f.line)
            };

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

AGENTS.md:83

  • The docs claim scan_extension(dir) -> Result<ScanReport>, but the implementation returns Result<ScanReport, String> (and callers handle a String error). This mismatch makes the API contract unclear for contributors.
- `scan_extension(dir) -> Result<ScanReport>`, `scan_bundle(content, path) -> Vec<Finding>`,
  `is_blocked(findings) -> bool`.

rust/src/extension/security/source.rs:40

  • scan_extension records each finding’s file using the full filesystem path (path.display()), while the post-bundle scanner uses the manifest-relative source string. This makes error output noisier and less stable across machines; consider storing paths relative to package_dir for pre-bundle findings.
        let source = std::fs::read_to_string(path)
            .map_err(|e| format!("failed to read '{}': {e}", path.display()))?;
        let path_str = path.display().to_string();
        findings.extend(scan_source_file(&path_str, &source)?);
    }

Comment thread rust/src/extension/security/file_discovery.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.

Suppressed comments (2)

rust/src/extension/security/util.rs:8

  • matches_http_url is case-sensitive, so strings like HTTP://example.com won't trigger SEC008 even though they are valid URLs. Since this is part of a security scanner, it should be robust against scheme casing.
pub fn matches_http_url(s: &str) -> bool {
    s.contains("http://") || s.contains("https://")
}

rust/src/extension/security/util.rs:27

  • offset_to_line_col returns a byte-based column (computed from byte offsets), but the Finding.col docs say it’s a 1-based column. For UTF-8 source this can misreport columns after multibyte characters; compute the column as a character count from the start of the line instead.
pub fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
    let offset = source.floor_char_boundary(offset.min(source.len()));
    let before = &source[..offset];
    let line = before.bytes().filter(|&b| b == b'\n').count() + 1;
    let col = match before.rfind('\n') {

@qcai-godaddy
qcai-godaddy marked this pull request as ready for review August 12, 2026 21:06
Comment thread PR-DEVEX-710.md Outdated
@jpage-godaddy

Copy link
Copy Markdown
Collaborator

@wcole1-godaddy is going to test this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants