SAST Scanning

Static Application Security Testing — Sectora reads your source code, finds vulnerabilities before they ship, and applies AI-grounded context that knows which findings are real and which are guarded.

What SAST does (and what it doesn't)

SAST analyzes your source codewithout running it. It walks the AST, traces variable flow, recognises taint sources (request input) flowing into dangerous sinks (SQL queries, exec calls), and reports the path. Catches bugs while they're still in the IDE — long before they reach production.

SAST does NOT catch issues that depend on runtime config (use DAST), vulnerable dependencies (use SCA), or leaked credentials in non-code files (use Secrets). Sectora bundles all four; the SAST pipeline is the one that reads your .go / .tsx / .py source.

Two engines, one pipeline

Sectora SAST runs two engines in sequence on every scan, then merges their findings through one triage pipeline:

  1. Semgrep — pattern-based AST matching across 30+ languages, with the public p/security-auditruleset PLUS Sectora's 6 custom rules (see below).
  2. goanalyzer — Sectora-built type-aware Go SAST engine. Uses go/parser + go/types + golang.org/x/tools/go/packagesfor real taint propagation (intra-procedural, inter-procedural depth 3, cross-package via summaries). What Semgrep's pattern matching can't do.

Findings from both flow through the same dedupe → mitigation-downscoring → fingerprint → AI Smart Triage chain.

Supported languages

Go (type-aware via goanalyzer)
TypeScript / TSX
JavaScript / JSX
Python
Java
C# / .NET
Ruby
PHP
Kotlin
Swift
Rust
C / C++
Scala
Solidity
Terraform / HCL
Bash
YAML / Dockerfile

Go gets the deepest analysis — type-aware taint tracking, sanitiser recognition, cross-function flow. Everything else uses Semgrep's pattern engine + AI Smart Triage on top.

Scan configuration (every option)

Repository

Pick from your connected GitHub / GitLab / Bitbucket repos in the dropdown. Connect a provider at Settings → Integrationsif your repo isn't listed. Private repos are supported on all three providers (uses the OAuth token you authorized at connect time).

Branch

The branch dropdown auto-populates from the repo's actual branches via the provider's API. Default-selects the repo's default branch (main / master / develop). Free-form typing is also supported for branches that haven't synced yet (just-pushed) or in edge cases where the API returns a subset.

Ruleset

Choose which Semgrep rule pack runs:

  • auto — Semgrep's curated rules for the detected languages. Sane default.
  • p/security-audit — Comprehensive web security rules (~500 rules). Default for new scans.
  • p/owasp-top-ten — Rules mapped to OWASP Top 10 categories.
  • p/cwe-top-25 — Top 25 most dangerous CWE classes.

Sectora's 6 custom rules (see below) ALWAYS run on top of whichever ruleset you pick.

Mode

  • Hybrid (recommended) — Semgrep + goanalyzer + AI Smart Triage. Best signal-to-noise.
  • Semgrep only — Skips AI triage. Faster, raw rule output. Useful when comparing to other Semgrep installations.

Exclude paths

Comma-separated glob patterns to skip (e.g. vendor/, *_test.go, dist/). The default exclude list already covers node_modules, vendor, .git, dist, build, __pycache__, .next, .terraform, *.min.js, test files, coverage reports, lock files, CI config files, codegen scripts, and .env* — extending the soltik FP-triage catalogue.

Sectora's custom rules

Bundled rules for attack surface the public Semgrep registry doesn't cover. Ship in every SAST image at /app/rules/sectora/:

ai-prompt-injection-system-prompt-concat
User input concatenated into LLM system prompts — the canonical "ignore previous instructions" vector.
mcp-server-unsanitized-tool-input
MCP server tool arguments flowing into fs / child_process / fetch / SQL without validation.
cloudflare-workers-env-secret-leak
env.SECRET / env.API_KEY / env.JWT_* appearing in Cloudflare Worker Response bodies.
nextjs-public-env-with-secret-name
NEXT_PUBLIC_*_SECRET / *_KEY / *_TOKEN — env vars baked into the client bundle that shouldn't be.
agent-tools-sandbox-bypass-js / -py
Agent framework tool definitions (Vercel AI SDK, LangChain, @tool decorator) that grant unsandboxed shell access.

Build-time semgrep --validate gates the rule set — a DSL-broken rule blocks the image build instead of silently failing at scan time.

goanalyzer (Go type-aware engine)

Built in-house because Semgrep's Go support is pattern-only — it can't follow taint across method calls or model type-parameter code. goanalyzer uses Go's stdlib (go/parser, go/types) + golang.org/x/tools/go/packages to build a fully-typed program representation in process. Tracks:

  • Sourcesr.URL.Query(), r.FormValue, r.Header.Get, r.Body, r.PathValue, r.Cookie, untyped json.Unmarshal. Env vars (os.Getenv) are NOT considered taint sources by default — they're operator config, not user input.
  • Sinksos/exec.Command (RCE), database/sql.Query/Exec + gorm.Raw (SQLi), os.Open/ReadFile (traversal), http.Get/Post/NewRequest (SSRF), http.Redirect (open redirect), html/template.Parse (template injection), regexp.Compile (ReDoS).
  • Sanitisers that clear taint — strconv.Atoi + ParseInt/Float (scalar return), uuid.Parse, time.Parse, net.ParseIP, html.EscapeString, url.QueryEscape, plus a structural rule: any call whose static return type is int/bool/floatcan't carry an injection payload.
  • Cross-package flow via summary cache. Walks call graphs to depth 3, caches per (function, taint-signature) so re-entry is O(1).

Findings prefixed go-* (e.g. go-sql-injection, go-command-injection, go-ssrf) so you can filter by engine in the UI.

AI Smart Triage

Every finding from Semgrep + goanalyzer flows through the AI Smart Triage pipeline before reaching your queue. The AI receives:

  • The finding metadata (rule, severity, file, line, CWE).
  • Whole-file context for files ≤ 500 lines AND ≤ 256 KB (small enough to ship the whole thing).
  • Otherwise: the enclosing function body (up to 300 lines), PLUS resolved bodies of any same-file helpers called from that function (up to 5 helpers, 50 lines each).
  • Sanitiser-shaped imports from the top of the file (escapeHtml, DOMPurify, etc.) as a context hint.
  • The Semgrep snippet as a fallback when the file can't be read.

Source files are wrapped in <untrusted_code> fences in the AI prompt, with the close-tag neutralised and the system prompt instructing the model to treat the fence content as data, not instructions — prompt-injection resistance.

The AI returns a structured verdict per finding: false_positive (bool), fp_confidence (0.0–1.0), risk_score (0–100), analysis (cited reasoning), and fix_suggestion. The UI shows AI-marked FPs as muted rows with the reasoning expanded inline — not hidden, so you can verify or override.

Post-processing filters

Three deterministic filters run on the merged finding list before AI Smart Triage:

  1. Known-FP allowlist — Sectora-specific rule+path patterns that are intentional behavior (e.g. use-of-md5 in internal/scanner/ is response-dedupe fingerprinting, not crypto; bypass-tls-verification in the scanner is intentional for testing broken-cert sites). Operator can extend per-customer.
  2. .js / .ts sibling dedup — When a monorepo ships compiled .js next to source .ts, the same finding shows up twice. Collapsed to the .ts version only.
  3. Sanitisation downscoring — Findings near a known sanitiser pattern (e.g. raw-html-format near escapeHtml(...); detect-non-literal-regexp near .replace(/[.*+?^$...]/g, ...)) drop from High to Low. Only fires on a conservative allow-list of rule_ids — never silently suppresses unknown patterns.

Fingerprint-based FP inheritance

Every finding gets a deterministic SHA-256 fingerprint derived from rule_id + file path + code snippet (canonicalised, line-number-independent). When you mark a finding as a false positive, that fingerprint is remembered. Subsequent scans on the same target check the fingerprint set and auto-mark matching findings as FP — no re-triage needed for the same bug.

Useful when refactoring: a known-FP that moves to a different line number (but same code shape) stays suppressed.

Cross-scanner correlation with DAST

The correlator joins SAST findings by URL/file pattern to live DAST vulnerabilities for the same user. When both engines agree, the finding is real (not an FP either side hallucinated).

CI/CD integration

PR gate

# .github/workflows/sectora-sast.yml
name: Sectora SAST
on: pull_request

jobs:
  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run SAST + block on new High+
        env:
          SECTORA_API_KEY: ${{ secrets.SECTORA_API_KEY }}
        run: |
          SCAN_ID=$(curl -sf -X POST https://api.sectora.io/api/v1/sast/scans \
            -H "Authorization: Bearer $SECTORA_API_KEY" \
            -d '{"target": "${{ github.repository }}", "branch": "${{ github.head_ref }}"}' \
            | jq -r .data.id)

          # Poll until complete
          for i in {1..40}; do
            STATUS=$(curl -sf -H "Authorization: Bearer $SECTORA_API_KEY" \
              https://api.sectora.io/api/v1/sast/scans/$SCAN_ID | jq -r .data.status)
            [ "$STATUS" = "completed" ] && break
            sleep 15
          done

          # Fail PR if scan introduced new High/Critical findings vs main
          curl -sf -H "Authorization: Bearer $SECTORA_API_KEY" \
            "https://api.sectora.io/api/v1/sast/scans/$SCAN_ID/diff?baseline=main&min_severity=high" \
            | jq -e '.new | length == 0'

See PR Gates for the full template + branch-protection wiring.

Troubleshooting

"Scan returned 0 findings on a real codebase"

We've seen this exact case. Check in this order:

  1. Open the scan log. Look for ℹ️ SAST raw: semgrep returned N findings, M errors, F files scanned, version="X". If N=0, Semgrep itself returned zero (look at errors next). If N>0 but the stored total is 0, the FP filter is suppressing everything.
  2. Look for ⚠️ SAST: semgrep error: [level=warn type=PartialParsing path=…]. PartialParsing means Semgrep's tree-sitter couldn't parse those files — usually big page components with deeply-nested JSX. They fall back to lexer-only mode which can miss some rules. Bumping maxFunctionWalkUp + maxFunctionBodyLines helps.
  3. Look for ℹ️ SAST filter: dropped rule=X path=Y reason=knownFalsePositive. The allowlist may be over-suppressing.
  4. For Go repos, confirm goanalyzer: analysed N packages / M functions appears. If 0/0, the Go toolchain couldn't load the module — check the repo's go.modversion against the image's Go version (currently 1.26.2).

"Too many findings — most are FPs"

  • Enable Hybrid mode (default) — without AI Smart Triage you're seeing raw Semgrep output.
  • The UI default shows AI-flagged FPs muted but visible. Click the chip to hide them entirely if you want a cleaner queue.
  • Add domain-specific known-FP patterns via Settings → SAST FP rules.

"Scan times out / runs slowly"

  • Large monorepos: expand Exclude paths to drop generated code, fixtures, vendored deps.
  • Switch ruleset from p/security-audit to p/owasp-top-ten for ~3× speed-up at the cost of low-severity coverage.
  • The scan-job Cloud Run container has 4 GB / 2 CPU; bump if your repo regularly exceeds 30 min.

What's next

  • DAST — the runtime companion to SAST.
  • SCA — dependency scanning that complements both.
  • PR Gates — block insecure code at merge time.
  • Risk Scoring — how Sectora prioritises findings across scanners.
  • Auto Virtual Patching — deploy a Shield rule from any finding.