secscan 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4c054fe23e6dea33fa17263f92926076622da170897a3fcb1aaf4db949a78a7e
4
+ data.tar.gz: d49426bba1e1a06e2db4fb22f55e944b4517d8bc6fa909f5a478e9e30c48f62e
5
+ SHA512:
6
+ metadata.gz: aeceefd29b13d24aeba3cd51182f2d01b67461ab5d3a62d671ed11a8c262242e5b392d78b9a2e5f9cc658b16d251e161d8cb913007065470ad4e5c5b9dd8a203
7
+ data.tar.gz: 685f08e49e85aefbd8a41ae7732b911baaf96267a341b0f01b0b615b9e2baa5bda8844e5969b2fa40cf47e83cbff8d14fc96c2a35c1f81bc9d84d624c1d7623a
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First gem of the [SecScan](https://github.com/saulofilho/secscan) SAST engine: secret and route rules, Shannon entropy, impact score, JSON/CSV/SARIF/Markdown reports, and a CLI quality gate.
6
+
7
+ Serialized reports mask the matched value. The in-memory object still keeps the literal.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Saulo Filho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,519 @@
1
+ # secscan
2
+
3
+ Static analysis engine extracted from [SecScan](https://github.com/saulofilho/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.
4
+
5
+ Ruby gem and CLI. The same engine ships as the Python package `secscan`. The React dashboard stays in the original repository.
6
+
7
+ ```bash
8
+ gem install secscan
9
+ secscan .
10
+ secscan . --fail-on high --max-risk 50
11
+ secscan . --format sarif --output secscan.sarif
12
+ ```
13
+
14
+ Serialized reports **mask** the matched value. Use `--reveal-secrets` only when the artifact is restricted. The in-memory `Finding` object still keeps the literal.
15
+
16
+ ## Contents
17
+
18
+ - [What it does](#what-it-does)
19
+ - [Use cases](#use-cases)
20
+ - [Proof of concept](#proof-of-concept)
21
+ - [Installation](#installation)
22
+ - [CLI](#cli)
23
+ - [Output formats](#output-formats)
24
+ - [Quality gates](#quality-gates)
25
+ - [Scoring](#scoring)
26
+ - [Built-in rules](#built-in-rules)
27
+ - [Custom rules](#custom-rules)
28
+ - [Ignore patterns](#ignore-patterns)
29
+ - [Programmatic API](#programmatic-api)
30
+ - [GitHub Actions](#github-actions)
31
+ - [What it does not do](#what-it-does-not-do)
32
+
33
+ ## What it does
34
+
35
+ | Capability | Detail |
36
+ |---|---|
37
+ | Secret scan | AWS, GCP/Gemini, GitHub, Stripe, JWT, Slack, private keys, database URIs, hardcoded passwords, OpenAI, SendGrid |
38
+ | Route scan | Admin/internal paths and hardcoded `/api`, `/v1`, `/graphql`, `/webhook` endpoints |
39
+ | Entropy | Shannon entropy; a match below `minEntropy` is dropped |
40
+ | File walk | `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`, `.json`, `.env`, `.yaml`, `.yml` |
41
+ | Ignore | `node_modules`, `vendor`, `dist`, `build`, lockfiles, plus `--ignore` |
42
+ | Score | Severity × file criticality, then an impact score from 0 to 100 |
43
+ | Reports | `table`, `json`, `csv`, `sarif`, `markdown` |
44
+ | CI gate | `--fail-on` and `--max-risk`, exit `1` |
45
+
46
+ ## Use cases
47
+
48
+ **Pre-commit or local review.** Scan a branch before you open a PR. The default table is meant for a terminal.
49
+
50
+ ```bash
51
+ secscan ./src --fail-on high
52
+ ```
53
+
54
+ **CI quality gate.** Block a merge when a critical secret lands, or when the workspace impact score goes above the team cap.
55
+
56
+ ```bash
57
+ secscan . --fail-on critical --max-risk 50 --format json --output secscan.json
58
+ ```
59
+
60
+ **GitHub Code Scanning.** Emit SARIF 2.1.0 and upload it so findings show on the Security tab.
61
+
62
+ ```bash
63
+ secscan . --format sarif --output secscan.sarif
64
+ ```
65
+
66
+ **Compliance export.** CSV for a spreadsheet, Markdown for an audit note.
67
+
68
+ ```bash
69
+ secscan . --format csv --output findings.csv
70
+ secscan . --format markdown --output findings.md
71
+ ```
72
+
73
+ **Library in a Ruby tool.** Call `Secscan.scan` or `Secscan.scan_text` from a Rake task, a bot, or a larger AppSec pipeline.
74
+
75
+ **Custom policy.** Add org-specific regex in a JSON file and merge it with the built-in set.
76
+
77
+ ```bash
78
+ secscan . --rules ./examples/poc/custom-rules.json
79
+ ```
80
+
81
+ ## Proof of concept
82
+
83
+ The repo ships a tiny tree under [`examples/poc`](examples/poc): one source file with public sample values (the official AWS example access key, a fake database URI, and an admin route).
84
+
85
+ ```bash
86
+ # from the gem root, after bundle install
87
+ bundle exec ruby -Ilib exe/secscan examples/poc --format table
88
+ ```
89
+
90
+ After `gem install secscan`:
91
+
92
+ ```bash
93
+ secscan examples/poc --format table
94
+ secscan examples/poc --format json
95
+ secscan examples/poc --fail-on high; echo $?
96
+ ```
97
+
98
+ The POC source is:
99
+
100
+ ```javascript
101
+ // Proof of concept only. Values are public samples, not live credentials.
102
+ const awsKey = "AKIAIOSFODNN7EXAMPLE";
103
+ const db = "postgres://admin:SuperSecretPass123@db.prod.internal:5432/main";
104
+
105
+ app.get("/api/v1/admin/users", handler);
106
+ ```
107
+
108
+ Expected findings (masked in every serialized format):
109
+
110
+ | Rule | Severity | Why it fires |
111
+ |---|---|---|
112
+ | `sec-aws-akid` | CRITICAL | AWS access key pattern |
113
+ | `sec-db-uri` | CRITICAL | Database URI with a password |
114
+ | `sec-sensitive-api-path` | MEDIUM | `/api/v1/admin/...` |
115
+ | `sec-api-endpoint` | LOW | Hardcoded `/api/...` path |
116
+
117
+ The same file also maps `GET /api/v1/admin/users` as a sensitive API endpoint.
118
+
119
+ A clean tree exits `0` and prints no findings:
120
+
121
+ ```bash
122
+ mkdir -p /tmp/secscan-clean && echo 'const ok = 1;' > /tmp/secscan-clean/app.js
123
+ secscan /tmp/secscan-clean --fail-on critical --format table
124
+ ```
125
+
126
+ ## Installation
127
+
128
+ ```bash
129
+ gem install secscan
130
+ ```
131
+
132
+ In a `Gemfile`:
133
+
134
+ ```ruby
135
+ gem "secscan"
136
+ ```
137
+
138
+ From this repository, without publishing:
139
+
140
+ ```bash
141
+ bundle install
142
+ bundle exec ruby -Ilib exe/secscan --version
143
+ ```
144
+
145
+ Requires Ruby 3.1 or newer.
146
+
147
+ ## CLI
148
+
149
+ ```text
150
+ Usage: secscan [path] [options]
151
+
152
+ --format FORMAT table (default), json, sarif, csv, markdown
153
+ --rules FILE extra rules JSON, merged with the built-in set
154
+ --ignore LIST extra ignore patterns, comma-separated
155
+ --fail-on LEVEL critical | high | medium | low | info
156
+ --max-risk SCORE fail if impact score (0-100) exceeds SCORE
157
+ --output FILE write the report to FILE instead of stdout
158
+ --reveal-secrets include the matched literal (restricted artifacts only)
159
+ -v, --version print version
160
+ ```
161
+
162
+ `path` may be a file or a directory. Default is `.`.
163
+
164
+ | Exit | Meaning |
165
+ |---|---|
166
+ | `0` | Scan finished; quality gate passed or was not set |
167
+ | `1` | Quality gate failed (`--fail-on` or `--max-risk`) |
168
+ | `2` | Invalid path, rules file, or CLI option |
169
+
170
+ `--output` writes the chosen format to a file and prints `Report written to …` on stderr. `--fail-on` / `--max-risk` still apply.
171
+
172
+ ## Output formats
173
+
174
+ Every serialized format masks secrets unless you pass `--reveal-secrets`. JSON never includes `matchedSecret` by default.
175
+
176
+ ### `table` (default)
177
+
178
+ Human-readable terminal report.
179
+
180
+ ```text
181
+ SecScan Static Security Analysis
182
+ Target: examples/poc | Files: 1 | Findings: 4 | 4ms
183
+ Impact: 66/100 (HIGH) | Security score: 43/100
184
+ ------------------------------------------------------------------------
185
+ CRITICAL [AWS Access Key ID]
186
+ File: src/app.js:2
187
+ Secret: AKIA••••••••••••MPLE (entropy 3.84)
188
+ Snippet: const awsKey = "AKIA••••••••••••MPLE";
189
+ Info: Chave de acesso pública da AWS encontrada hardcoded no código.
190
+ ```
191
+
192
+ ### `json`
193
+
194
+ Machine-readable report for CI, bots, and later processing.
195
+
196
+ ```json
197
+ {
198
+ "scanner": "SecScan SAST",
199
+ "version": "0.1.0",
200
+ "target": "examples/poc",
201
+ "totalFiles": 1,
202
+ "scannedFilesCount": 1,
203
+ "ignoredFilesCount": 0,
204
+ "findings": [
205
+ {
206
+ "id": "finding-1",
207
+ "ruleId": "sec-aws-akid",
208
+ "ruleName": "AWS Access Key ID",
209
+ "category": "CLOUD_CREDENTIAL",
210
+ "severity": "CRITICAL",
211
+ "file": "src/app.js",
212
+ "line": 2,
213
+ "column": 17,
214
+ "snippet": "const awsKey = \"AKIA••••••••••••MPLE\";",
215
+ "maskedSecret": "AKIA••••••••••••MPLE",
216
+ "entropy": 3.84,
217
+ "fileCriticality": "MEDIUM",
218
+ "fileCriticalityWeight": 1.0,
219
+ "weightedScore": 25.0
220
+ }
221
+ ],
222
+ "apiEndpoints": [
223
+ {
224
+ "id": "endpoint-1",
225
+ "file": "src/app.js",
226
+ "line": 5,
227
+ "method": "GET",
228
+ "path": "/api/v1/admin/users",
229
+ "isInternalOrAdmin": true
230
+ }
231
+ ],
232
+ "metrics": {
233
+ "criticalCount": 2,
234
+ "highCount": 0,
235
+ "mediumCount": 1,
236
+ "lowCount": 1,
237
+ "infoCount": 0,
238
+ "securityScore": 43,
239
+ "securityImpactScore": 66,
240
+ "impactLevel": "HIGH",
241
+ "totalWeightedRisk": 60.0,
242
+ "averageEntropy": 4.12
243
+ },
244
+ "durationMs": 4
245
+ }
246
+ ```
247
+
248
+ Finding fields: `id`, `ruleId`, `ruleName`, `category`, `severity`, `file`, `line`, `column`, `snippet`, `maskedSecret`, `entropy`, `description`, `remediation`, `fileCriticality`, `fileCriticalityWeight`, `weightedScore`. With `--reveal-secrets`, `matchedSecret` is added.
249
+
250
+ ### `csv`
251
+
252
+ One row per finding. Header:
253
+
254
+ ```text
255
+ ID,Severity,Rule Name,Category,File,Line,Column,Entropy,Masked Secret,Remediation
256
+ ```
257
+
258
+ ```text
259
+ finding-1,CRITICAL,AWS Access Key ID,CLOUD_CREDENTIAL,src/app.js,2,17,3.84,AKIA••••••••••••MPLE,Utilize AWS IAM Roles...
260
+ ```
261
+
262
+ Useful in Sheets or Excel. Endpoints are not included; use JSON if you need them.
263
+
264
+ ### `sarif`
265
+
266
+ OASIS SARIF 2.1.0 for GitHub Code Scanning (`github/codeql-action/upload-sarif`).
267
+
268
+ ```json
269
+ {
270
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
271
+ "version": "2.1.0",
272
+ "runs": [
273
+ {
274
+ "tool": {
275
+ "driver": {
276
+ "name": "SecScan",
277
+ "semanticVersion": "0.1.0",
278
+ "informationUri": "https://github.com/saulofilho/secscan"
279
+ }
280
+ },
281
+ "results": [
282
+ {
283
+ "ruleId": "sec-aws-akid",
284
+ "level": "error",
285
+ "message": { "text": "AWS Access Key ID. Value: AKIA••••••••••••MPLE" },
286
+ "locations": [
287
+ {
288
+ "physicalLocation": {
289
+ "artifactLocation": { "uri": "src/app.js" },
290
+ "region": { "startLine": 2, "startColumn": 17 }
291
+ }
292
+ }
293
+ ]
294
+ }
295
+ ]
296
+ }
297
+ ]
298
+ }
299
+ ```
300
+
301
+ `CRITICAL` and `HIGH` map to SARIF `error`. Everything else maps to `warning`.
302
+
303
+ ### `markdown`
304
+
305
+ Audit-style document for pull requests or tickets.
306
+
307
+ ```markdown
308
+ # SecScan report
309
+
310
+ - **Target:** `examples/poc`
311
+ - **Files scanned:** 1 (ignored: 0)
312
+ - **Findings:** 4
313
+ - **Security score:** 43/100
314
+ - **Impact score:** 66/100 (HIGH)
315
+
316
+ ## Findings
317
+
318
+ ### [CRITICAL] AWS Access Key ID
319
+ - **File:** `src/app.js` (line 2)
320
+ - **Value:** `AKIA••••••••••••MPLE`
321
+ - **Entropy:** 3.84
322
+ ```
323
+
324
+ Impact and file counts in the samples above are representative of `examples/poc`. Entropy and duration can vary slightly by platform.
325
+
326
+ ## Quality gates
327
+
328
+ `--fail-on` uses this order: `info` < `low` < `medium` < `high` < `critical`. A finding at the chosen level **or above** fails the process.
329
+
330
+ ```bash
331
+ secscan . --fail-on critical # only CRITICAL fails the build
332
+ secscan . --fail-on high # HIGH and CRITICAL fail
333
+ secscan . --max-risk 50 # impact score 51+ fails
334
+ secscan . --fail-on high --max-risk 50
335
+ ```
336
+
337
+ Stderr on failure:
338
+
339
+ ```text
340
+ SecScan quality gate: findings with severity >= HIGH
341
+ SecScan quality gate: impact score 66 above 50
342
+ ```
343
+
344
+ A scan with findings still exits `0` if you set neither flag.
345
+
346
+ ## Scoring
347
+
348
+ Each finding gets `weightedScore = severityWeight × fileCriticality`.
349
+
350
+ | Severity | Weight |
351
+ |---|---|
352
+ | CRITICAL | 25 |
353
+ | HIGH | 14 |
354
+ | MEDIUM | 7 |
355
+ | LOW | 3 |
356
+ | INFO | 1 |
357
+
358
+ | File class | Multiplier | Examples |
359
+ |---|---|---|
360
+ | CRITICAL | 2.0 | `.env`, secrets, keys, `Dockerfile`, `config/` |
361
+ | HIGH | 1.5 | `services/`, `routes/`, `auth`, payments |
362
+ | MEDIUM | 1.0 | regular application code |
363
+ | LOW | 0.5 | tests, docs, fixtures |
364
+
365
+ ```text
366
+ impactScore = 100 × (1 − e^(−totalWeightedRisk / 55))
367
+ ```
368
+
369
+ | Impact score | Level |
370
+ |---|---|
371
+ | 0 | NOMINAL |
372
+ | 1–14 | LOW |
373
+ | 15–34 | MODERATE |
374
+ | 35–59 | ELEVATED |
375
+ | 60–79 | HIGH |
376
+ | 80–100 | CRITICAL |
377
+
378
+ `securityScore` starts at 100 and subtracts `25` per CRITICAL, `12` per HIGH, `5` per MEDIUM, `2` per LOW.
379
+
380
+ ## Built-in rules
381
+
382
+ | ID | Severity | Category |
383
+ |---|---|---|
384
+ | `sec-aws-akid` | CRITICAL | CLOUD_CREDENTIAL |
385
+ | `sec-aws-secret` | CRITICAL | CLOUD_CREDENTIAL |
386
+ | `sec-github-pat` | CRITICAL | AUTH_TOKEN |
387
+ | `sec-stripe-secret` | CRITICAL | API_KEY |
388
+ | `sec-private-key` | CRITICAL | PRIVATE_KEY |
389
+ | `sec-db-uri` | CRITICAL | DATABASE_URI |
390
+ | `sec-openai-key` | CRITICAL | API_KEY |
391
+ | `sec-google-api` | HIGH | API_KEY |
392
+ | `sec-jwt-token` | HIGH | AUTH_TOKEN |
393
+ | `sec-slack-webhook` | HIGH | AUTH_TOKEN |
394
+ | `sec-hardcoded-pass` | HIGH | PASSWORD |
395
+ | `sec-sendgrid-key` | HIGH | API_KEY |
396
+ | `sec-sensitive-api-path` | MEDIUM | API_PATH |
397
+ | `sec-api-endpoint` | LOW | API_PATH |
398
+
399
+ Rule descriptions and remediations currently stay in Portuguese inside the finding payload. IDs, names, severities, and categories are English.
400
+
401
+ ## Custom rules
402
+
403
+ `--rules` loads a JSON **array** and **appends** it to the built-in set. See [`examples/poc/custom-rules.json`](examples/poc/custom-rules.json).
404
+
405
+ ```json
406
+ [
407
+ {
408
+ "id": "sec-demo-token",
409
+ "name": "Demo corp token",
410
+ "pattern": "\\bCORP-[A-Z0-9]{24}\\b",
411
+ "severity": "HIGH",
412
+ "category": "CUSTOM",
413
+ "description": "Internal token format for the POC.",
414
+ "remediation": "Move CORP-* tokens to an environment variable.",
415
+ "flags": "g",
416
+ "minEntropy": 3.0
417
+ }
418
+ ]
419
+ ```
420
+
421
+ | Field | Required | Notes |
422
+ |---|---|---|
423
+ | `id` | yes | Stable rule id |
424
+ | `name` | yes | Shown in reports |
425
+ | `pattern` | yes | Ruby/JavaScript-style regular expression |
426
+ | `severity` | no | `INFO`, `LOW`, `MEDIUM`, `HIGH`, `CRITICAL` (default `MEDIUM`) |
427
+ | `category` | no | Default `CUSTOM` |
428
+ | `description` | no | |
429
+ | `remediation` | no | |
430
+ | `flags` | no | `g`, `i`, or `gi`. `(?i)` prefixes are accepted |
431
+ | `minEntropy` / `min_entropy` | no | Drop matches below this Shannon value |
432
+ | `enabled` | no | Default `true` |
433
+
434
+ An invalid file exits `2` (`rules file not found` or `rules file is not valid JSON`).
435
+
436
+ ## Ignore patterns
437
+
438
+ 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`.
439
+
440
+ `--ignore` adds comma-separated patterns:
441
+
442
+ ```bash
443
+ secscan . --ignore "tests/*,docs/*,*.spec.ts"
444
+ ```
445
+
446
+ Supported shapes: `tests/*`, `vendor/**`, `*.test.*`, `package-lock.json`, or a directory name such as `fixtures`.
447
+
448
+ ## Programmatic API
449
+
450
+ ```ruby
451
+ require "secscan"
452
+
453
+ report = Secscan.scan("./src", ignore: ["tests/*"])
454
+ report.metrics.security_impact_score # 0..100
455
+ report.metrics.impact_level # NOMINAL, LOW, MODERATE, ELEVATED, HIGH, CRITICAL
456
+ report.findings.each do |finding|
457
+ puts "#{finding.severity} #{finding.file}:#{finding.line} #{finding.masked_secret}"
458
+ end
459
+
460
+ inline = Secscan.scan_text(<<~JS, path: "src/app.js")
461
+ const key = "AKIAIOSFODNN7EXAMPLE";
462
+ JS
463
+
464
+ rules = Secscan.load_rules("examples/poc/custom-rules.json")
465
+ Secscan.scan(".", rules: rules)
466
+
467
+ Secscan.calculate_entropy("AKIAIOSFODNN7EXAMPLE")
468
+ Secscan.mask_secret("AKIAIOSFODNN7EXAMPLE")
469
+ # => "AKIA••••••••••••MPLE"
470
+
471
+ text = Secscan::Report.render(report, "sarif")
472
+ ```
473
+
474
+ `Secscan.scan` accepts a file or a directory. `scan_text` scans one string. Both return a `ScanReport`.
475
+
476
+ ## GitHub Actions
477
+
478
+ ```yaml
479
+ name: SecScan
480
+
481
+ on:
482
+ push:
483
+ pull_request:
484
+
485
+ permissions:
486
+ contents: read
487
+ security-events: write
488
+
489
+ jobs:
490
+ sast:
491
+ runs-on: ubuntu-latest
492
+ steps:
493
+ - uses: actions/checkout@v4
494
+ - uses: ruby/setup-ruby@v1
495
+ with:
496
+ ruby-version: "3.3"
497
+ - run: gem install secscan
498
+ - name: Scan
499
+ run: |
500
+ secscan . \
501
+ --format sarif \
502
+ --output secscan.sarif \
503
+ --ignore "tests/*,docs/*"
504
+ - uses: github/codeql-action/upload-sarif@v3
505
+ if: always()
506
+ with:
507
+ sarif_file: secscan.sarif
508
+ - name: Quality gate
509
+ run: secscan . --fail-on critical --max-risk 50
510
+ ```
511
+
512
+ ## What it does not do
513
+
514
+ - DAST, fuzzing, live HTTP attacks, WAF, or EDR
515
+ - Dynamic confirmation that a secret is still valid
516
+ - Auto-remediation patches
517
+ - Skills, agents, or MCP (next step)
518
+
519
+ License: MIT. Changelog: [CHANGELOG.md](CHANGELOG.md).
@@ -0,0 +1,13 @@
1
+ [
2
+ {
3
+ "id": "sec-demo-token",
4
+ "name": "Demo corp token",
5
+ "pattern": "\\bCORP-[A-Z0-9]{24}\\b",
6
+ "severity": "HIGH",
7
+ "category": "CUSTOM",
8
+ "description": "Internal token format for the POC.",
9
+ "remediation": "Move CORP-* tokens to an environment variable.",
10
+ "flags": "g",
11
+ "minEntropy": 3.0
12
+ }
13
+ ]
@@ -0,0 +1,5 @@
1
+ // Proof of concept only. Values are public samples, not live credentials.
2
+ const awsKey = "AKIAIOSFODNN7EXAMPLE";
3
+ const db = "postgres://admin:SuperSecretPass123@db.prod.internal:5432/main";
4
+
5
+ app.get("/api/v1/admin/users", handler);
data/exe/secscan ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5
+ require "secscan/cli"
6
+
7
+ exit Secscan::CLI.run(ARGV)
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Secscan
6
+ class CLI
7
+ FORMATS = %w[table json sarif csv markdown].freeze
8
+ FAIL_LEVELS = %w[critical high medium low info].freeze
9
+
10
+ def self.run(argv = ARGV)
11
+ new.run(argv)
12
+ end
13
+
14
+ def run(argv)
15
+ options = {
16
+ format: "table",
17
+ rules: nil,
18
+ ignore: [],
19
+ fail_on: nil,
20
+ max_risk: nil,
21
+ output: nil,
22
+ reveal_secrets: false,
23
+ version: false
24
+ }
25
+ parser = build_parser(options)
26
+ parser.parse!(argv)
27
+ if options[:version]
28
+ puts "secscan #{VERSION}"
29
+ return 0
30
+ end
31
+ target = argv[0] || "."
32
+
33
+ rules = options[:rules] ? Scanner.load_rules(options[:rules]) : nil
34
+ report = Scanner.scan_path(target, rules: rules, ignore: options[:ignore])
35
+ text = Report.render(report, options[:format], reveal_secrets: options[:reveal_secrets])
36
+
37
+ if options[:output]
38
+ File.write(options[:output], text)
39
+ warn "Report written to #{options[:output]}"
40
+ else
41
+ $stdout.write(text)
42
+ end
43
+
44
+ failure = gate_message(report, options[:fail_on], options[:max_risk])
45
+ if failure
46
+ warn failure
47
+ return 1
48
+ end
49
+ 0
50
+ rescue OptionParser::ParseError, InputError => e
51
+ warn "Error: #{e.message}"
52
+ 2
53
+ end
54
+
55
+ def gate_message(report, fail_on, max_risk)
56
+ reasons = []
57
+ if fail_on
58
+ threshold = SEVERITY_RANK.fetch(fail_on.upcase)
59
+ if report.findings.any? { |finding| SEVERITY_RANK.fetch(finding.severity) >= threshold }
60
+ reasons << "findings with severity >= #{fail_on.upcase}"
61
+ end
62
+ end
63
+ if !max_risk.nil? && report.metrics.security_impact_score > max_risk
64
+ reasons << "impact score #{report.metrics.security_impact_score} acima de #{max_risk}"
65
+ end
66
+ return nil if reasons.empty?
67
+
68
+ "SecScan quality gate: #{reasons.join('; ')}"
69
+ end
70
+
71
+ def build_parser(options)
72
+ OptionParser.new do |opts|
73
+ opts.banner = <<~BANNER
74
+ Usage: secscan [path] [options]
75
+
76
+ Static analysis for secrets, entropy, and API paths.
77
+
78
+ BANNER
79
+ opts.on("--format FORMAT", FORMATS, "table, json, sarif, csv, markdown") { |value| options[:format] = value }
80
+ opts.on("--rules FILE", "JSON file with extra rules") { |value| options[:rules] = value }
81
+ opts.on("--ignore LIST", "Extra ignore patterns, comma-separated") do |value|
82
+ options[:ignore] = value.split(",").map(&:strip).reject(&:empty?)
83
+ end
84
+ opts.on("--fail-on LEVEL", FAIL_LEVELS, "Exit 1 if a finding meets or exceeds this severity") do |value|
85
+ options[:fail_on] = value
86
+ end
87
+ opts.on("--max-risk SCORE", Float, "Exit 1 if the impact score exceeds the cap") { |value| options[:max_risk] = value }
88
+ opts.on("--output FILE", "Write the report to this file") { |value| options[:output] = value }
89
+ opts.on("--reveal-secrets", "Include the matched value in the report") { options[:reveal_secrets] = true }
90
+ opts.on("-v", "--version", "Print version") { options[:version] = true }
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Secscan
4
+ class Error < StandardError; end
5
+
6
+ class InputError < Error; end
7
+ end