Skip to content

About

Static analysis engine extracted from SecScan. It walks JavaScript and TypeScript trees, looks for hardcoded secrets and sensitive API paths, scores the workspace, and fails CI when a policy is exceeded.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

secscan

Static analysis engine extracted from SecScan. It walks JavaScript and TypeScript trees, looks for hardcoded secrets and sensitive API paths, scores the workspace, and fails CI when a policy is exceeded.

Ruby gem and CLI. The same engine ships as the Python package secscan. The React dashboard stays in the original repository.

gem install secscan
secscan .
secscan . --fail-on high --max-risk 50
secscan . --format sarif --output secscan.sarif

Serialized reports mask the matched value. Use --reveal-secrets only when the artifact is restricted. The in-memory Finding object still keeps the literal.

Contents

What it does

Capability Detail
Secret scan AWS, GCP/Gemini, GitHub, Stripe, JWT, Slack, private keys, database URIs, hardcoded passwords, OpenAI, SendGrid
Route scan Admin/internal paths and hardcoded /api, /v1, /graphql, /webhook endpoints
Entropy Shannon entropy; a match below minEntropy is dropped
File walk .js, .jsx, .ts, .tsx, .mjs, .cjs, .json, .env, .yaml, .yml
Ignore node_modules, vendor, dist, build, lockfiles, plus --ignore
Score Severity × file criticality, then an impact score from 0 to 100
Reports table, json, csv, sarif, markdown
CI gate --fail-on and --max-risk, exit 1

Use cases

Pre-commit or local review. Scan a branch before you open a PR. The default table is meant for a terminal.

secscan ./src --fail-on high

CI quality gate. Block a merge when a critical secret lands, or when the workspace impact score goes above the team cap.

secscan . --fail-on critical --max-risk 50 --format json --output secscan.json

GitHub Code Scanning. Emit SARIF 2.1.0 and upload it so findings show on the Security tab.

secscan . --format sarif --output secscan.sarif

Compliance export. CSV for a spreadsheet, Markdown for an audit note.

secscan . --format csv --output findings.csv
secscan . --format markdown --output findings.md

Library in a Ruby tool. Call Secscan.scan or Secscan.scan_text from a Rake task, a bot, or a larger AppSec pipeline.

Custom policy. Add org-specific regex in a JSON file and merge it with the built-in set.

secscan . --rules ./examples/poc/custom-rules.json

Proof of concept

The repo ships a tiny tree under examples/poc: one source file with public sample values (the official AWS example access key, a fake database URI, and an admin route).

# from the gem root, after bundle install
bundle exec ruby -Ilib exe/secscan examples/poc --format table

After gem install secscan:

secscan examples/poc --format table
secscan examples/poc --format json
secscan examples/poc --fail-on high; echo $?

The POC source is:

// Proof of concept only. Values are public samples, not live credentials.
const awsKey = "AKIAIOSFODNN7EXAMPLE";
const db = "postgres://admin:SuperSecretPass123@db.prod.internal:5432/main";

app.get("/api/v1/admin/users", handler);

Expected findings (masked in every serialized format):

Rule Severity Why it fires
sec-aws-akid CRITICAL AWS access key pattern
sec-db-uri CRITICAL Database URI with a password
sec-sensitive-api-path MEDIUM /api/v1/admin/...
sec-api-endpoint LOW Hardcoded /api/... path

The same file also maps GET /api/v1/admin/users as a sensitive API endpoint.

A clean tree exits 0 and prints no findings:

mkdir -p /tmp/secscan-clean && echo 'const ok = 1;' > /tmp/secscan-clean/app.js
secscan /tmp/secscan-clean --fail-on critical --format table

Installation

gem install secscan

In a Gemfile:

gem "secscan"

From this repository, without publishing:

bundle install
bundle exec ruby -Ilib exe/secscan --version

Requires Ruby 3.1 or newer.

CLI

Usage: secscan [path] [options]

  --format FORMAT        table (default), json, sarif, csv, markdown
  --rules FILE           extra rules JSON, merged with the built-in set
  --ignore LIST          extra ignore patterns, comma-separated
  --fail-on LEVEL        critical | high | medium | low | info
  --max-risk SCORE       fail if impact score (0-100) exceeds SCORE
  --output FILE          write the report to FILE instead of stdout
  --reveal-secrets       include the matched literal (restricted artifacts only)
  -v, --version          print version

path may be a file or a directory. Default is ..

Exit Meaning
0 Scan finished; quality gate passed or was not set
1 Quality gate failed (--fail-on or --max-risk)
2 Invalid path, rules file, or CLI option

--output writes the chosen format to a file and prints Report written to … on stderr. --fail-on / --max-risk still apply.

Output formats

Every serialized format masks secrets unless you pass --reveal-secrets. JSON never includes matchedSecret by default.

table (default)

Human-readable terminal report.

SecScan Static Security Analysis
Target: examples/poc | Files: 1 | Findings: 4 | 4ms
Impact: 66/100 (HIGH) | Security score: 43/100
------------------------------------------------------------------------
CRITICAL [AWS Access Key ID]
  File:    src/app.js:2
  Secret:  AKIA••••••••••••MPLE (entropy 3.84)
  Snippet: const awsKey = "AKIA••••••••••••MPLE";
  Info:    Chave de acesso pública da AWS encontrada hardcoded no código.

json

Machine-readable report for CI, bots, and later processing.

{
  "scanner": "SecScan SAST",
  "version": "0.1.0",
  "target": "examples/poc",
  "totalFiles": 1,
  "scannedFilesCount": 1,
  "ignoredFilesCount": 0,
  "findings": [
    {
      "id": "finding-1",
      "ruleId": "sec-aws-akid",
      "ruleName": "AWS Access Key ID",
      "category": "CLOUD_CREDENTIAL",
      "severity": "CRITICAL",
      "file": "src/app.js",
      "line": 2,
      "column": 17,
      "snippet": "const awsKey = \"AKIA••••••••••••MPLE\";",
      "maskedSecret": "AKIA••••••••••••MPLE",
      "entropy": 3.84,
      "fileCriticality": "MEDIUM",
      "fileCriticalityWeight": 1.0,
      "weightedScore": 25.0
    }
  ],
  "apiEndpoints": [
    {
      "id": "endpoint-1",
      "file": "src/app.js",
      "line": 5,
      "method": "GET",
      "path": "/api/v1/admin/users",
      "isInternalOrAdmin": true
    }
  ],
  "metrics": {
    "criticalCount": 2,
    "highCount": 0,
    "mediumCount": 1,
    "lowCount": 1,
    "infoCount": 0,
    "securityScore": 43,
    "securityImpactScore": 66,
    "impactLevel": "HIGH",
    "totalWeightedRisk": 60.0,
    "averageEntropy": 4.12
  },
  "durationMs": 4
}

Finding fields: id, ruleId, ruleName, category, severity, file, line, column, snippet, maskedSecret, entropy, description, remediation, fileCriticality, fileCriticalityWeight, weightedScore. With --reveal-secrets, matchedSecret is added.

csv

One row per finding. Header:

ID,Severity,Rule Name,Category,File,Line,Column,Entropy,Masked Secret,Remediation
finding-1,CRITICAL,AWS Access Key ID,CLOUD_CREDENTIAL,src/app.js,2,17,3.84,AKIA••••••••••••MPLE,Utilize AWS IAM Roles...

Useful in Sheets or Excel. Endpoints are not included; use JSON if you need them.

sarif

OASIS SARIF 2.1.0 for GitHub Code Scanning (github/codeql-action/upload-sarif).

{
  "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
  "version": "2.1.0",
  "runs": [
    {
      "tool": {
        "driver": {
          "name": "SecScan",
          "semanticVersion": "0.1.0",
          "informationUri": "https://github.com/saulofilho/secscan"
        }
      },
      "results": [
        {
          "ruleId": "sec-aws-akid",
          "level": "error",
          "message": { "text": "AWS Access Key ID. Value: AKIA••••••••••••MPLE" },
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": { "uri": "src/app.js" },
                "region": { "startLine": 2, "startColumn": 17 }
              }
            }
          ]
        }
      ]
    }
  ]
}

CRITICAL and HIGH map to SARIF error. Everything else maps to warning.

markdown

Audit-style document for pull requests or tickets.

# SecScan report

- **Target:** `examples/poc`
- **Files scanned:** 1 (ignored: 0)
- **Findings:** 4
- **Security score:** 43/100
- **Impact score:** 66/100 (HIGH)

## Findings

### [CRITICAL] AWS Access Key ID
- **File:** `src/app.js` (line 2)
- **Value:** `AKIA••••••••••••MPLE`
- **Entropy:** 3.84

Impact and file counts in the samples above are representative of examples/poc. Entropy and duration can vary slightly by platform.

Quality gates

--fail-on uses this order: info < low < medium < high < critical. A finding at the chosen level or above fails the process.

secscan . --fail-on critical    # only CRITICAL fails the build
secscan . --fail-on high        # HIGH and CRITICAL fail
secscan . --max-risk 50         # impact score 51+ fails
secscan . --fail-on high --max-risk 50

Stderr on failure:

SecScan quality gate: findings with severity >= HIGH
SecScan quality gate: impact score 66 above 50

A scan with findings still exits 0 if you set neither flag.

Scoring

Each finding gets weightedScore = severityWeight × fileCriticality.

Severity Weight
CRITICAL 25
HIGH 14
MEDIUM 7
LOW 3
INFO 1
File class Multiplier Examples
CRITICAL 2.0 .env, secrets, keys, Dockerfile, config/
HIGH 1.5 services/, routes/, auth, payments
MEDIUM 1.0 regular application code
LOW 0.5 tests, docs, fixtures
impactScore = 100 × (1 − e^(−totalWeightedRisk / 55))
Impact score Level
0 NOMINAL
1–14 LOW
15–34 MODERATE
35–59 ELEVATED
60–79 HIGH
80–100 CRITICAL

securityScore starts at 100 and subtracts 25 per CRITICAL, 12 per HIGH, 5 per MEDIUM, 2 per LOW.

Built-in rules

ID Severity Category
sec-aws-akid CRITICAL CLOUD_CREDENTIAL
sec-aws-secret CRITICAL CLOUD_CREDENTIAL
sec-github-pat CRITICAL AUTH_TOKEN
sec-stripe-secret CRITICAL API_KEY
sec-private-key CRITICAL PRIVATE_KEY
sec-db-uri CRITICAL DATABASE_URI
sec-openai-key CRITICAL API_KEY
sec-google-api HIGH API_KEY
sec-jwt-token HIGH AUTH_TOKEN
sec-slack-webhook HIGH AUTH_TOKEN
sec-hardcoded-pass HIGH PASSWORD
sec-sendgrid-key HIGH API_KEY
sec-sensitive-api-path MEDIUM API_PATH
sec-api-endpoint LOW API_PATH

Rule descriptions and remediations currently stay in Portuguese inside the finding payload. IDs, names, severities, and categories are English.

Custom rules

--rules loads a JSON array and appends it to the built-in set. See examples/poc/custom-rules.json.

[
  {
    "id": "sec-demo-token",
    "name": "Demo corp token",
    "pattern": "\\bCORP-[A-Z0-9]{24}\\b",
    "severity": "HIGH",
    "category": "CUSTOM",
    "description": "Internal token format for the POC.",
    "remediation": "Move CORP-* tokens to an environment variable.",
    "flags": "g",
    "minEntropy": 3.0
  }
]
Field Required Notes
id yes Stable rule id
name yes Shown in reports
pattern yes Ruby/JavaScript-style regular expression
severity no INFO, LOW, MEDIUM, HIGH, CRITICAL (default MEDIUM)
category no Default CUSTOM
description no
remediation no
flags no g, i, or gi. (?i) prefixes are accepted
minEntropy / min_entropy no Drop matches below this Shannon value
enabled no Default true

An invalid file exits 2 (rules file not found or rules file is not valid JSON).

Ignore patterns

Always skipped: node_modules, vendor, bower_components, .git, dist, build, out, coverage, .cache, *.min.js, *.bundle.js, package-lock.json, yarn.lock, pnpm-lock.yaml.

--ignore adds comma-separated patterns:

secscan . --ignore "tests/*,docs/*,*.spec.ts"

Supported shapes: tests/*, vendor/**, *.test.*, package-lock.json, or a directory name such as fixtures.

Programmatic API

require "secscan"

report = Secscan.scan("./src", ignore: ["tests/*"])
report.metrics.security_impact_score  # 0..100
report.metrics.impact_level           # NOMINAL, LOW, MODERATE, ELEVATED, HIGH, CRITICAL
report.findings.each do |finding|
  puts "#{finding.severity} #{finding.file}:#{finding.line} #{finding.masked_secret}"
end

inline = Secscan.scan_text(<<~JS, path: "src/app.js")
  const key = "AKIAIOSFODNN7EXAMPLE";
JS

rules = Secscan.load_rules("examples/poc/custom-rules.json")
Secscan.scan(".", rules: rules)

Secscan.calculate_entropy("AKIAIOSFODNN7EXAMPLE")
Secscan.mask_secret("AKIAIOSFODNN7EXAMPLE")
# => "AKIA••••••••••••MPLE"

text = Secscan::Report.render(report, "sarif")

Secscan.scan accepts a file or a directory. scan_text scans one string. Both return a ScanReport.

GitHub Actions

name: SecScan

on:
  push:
  pull_request:

permissions:
  contents: read
  security-events: write

jobs:
  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"
      - run: gem install secscan
      - name: Scan
        run: |
          secscan . \
            --format sarif \
            --output secscan.sarif \
            --ignore "tests/*,docs/*"
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: secscan.sarif
      - name: Quality gate
        run: secscan . --fail-on critical --max-risk 50

What it does not do

  • DAST, fuzzing, live HTTP attacks, WAF, or EDR
  • Dynamic confirmation that a secret is still valid
  • Auto-remediation patches
  • Skills, agents, or MCP (next step)

License: MIT. Changelog: CHANGELOG.md.

About

Static analysis engine extracted from SecScan. It walks JavaScript and TypeScript trees, looks for hardcoded secrets and sensitive API paths, scores the workspace, and fails CI when a policy is exceeded.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages