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 +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +519 -0
- data/examples/poc/custom-rules.json +13 -0
- data/examples/poc/src/app.js +5 -0
- data/exe/secscan +7 -0
- data/lib/secscan/cli.rb +94 -0
- data/lib/secscan/errors.rb +7 -0
- data/lib/secscan/report.rb +249 -0
- data/lib/secscan/rules.rb +205 -0
- data/lib/secscan/scanner.rb +438 -0
- data/lib/secscan/version.rb +5 -0
- data/lib/secscan.rb +30 -0
- metadata +90 -0
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
|
+
]
|
data/exe/secscan
ADDED
data/lib/secscan/cli.rb
ADDED
|
@@ -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
|