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.sarifSerialized reports mask the matched value. Use --reveal-secrets only when the artifact is restricted. The in-memory Finding object still keeps the literal.
- What it does
- Use cases
- Proof of concept
- Installation
- CLI
- Output formats
- Quality gates
- Scoring
- Built-in rules
- Custom rules
- Ignore patterns
- Programmatic API
- GitHub Actions
- What it does not do
| 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 |
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 highCI 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.jsonGitHub Code Scanning. Emit SARIF 2.1.0 and upload it so findings show on the Security tab.
secscan . --format sarif --output secscan.sarifCompliance export. CSV for a spreadsheet, Markdown for an audit note.
secscan . --format csv --output findings.csv
secscan . --format markdown --output findings.mdLibrary 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.jsonThe 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 tableAfter 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 tablegem install secscanIn a Gemfile:
gem "secscan"From this repository, without publishing:
bundle install
bundle exec ruby -Ilib exe/secscan --versionRequires Ruby 3.1 or newer.
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.
Every serialized format masks secrets unless you pass --reveal-secrets. JSON never includes matchedSecret by 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.
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.
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.
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.
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.84Impact and file counts in the samples above are representative of examples/poc. Entropy and duration can vary slightly by platform.
--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 50Stderr 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.
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.
| 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.
--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).
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.
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.
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- 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.