tc-scanner 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.
- package/.env +1 -0
- package/README.md +131 -0
- package/bin/cli.js +133 -0
- package/bitbucket-pipelines.example.yml +38 -0
- package/package.json +35 -0
- package/reports/scan-report.html +61 -0
- package/reports/scan-report.json +21 -0
- package/reports/scan-report.txt +20 -0
- package/samples/Dockerfile +17 -0
- package/src/scanner.js +525 -0
package/.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
NPM_TOKEN=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
|
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# tc-scanner
|
|
2
|
+
|
|
3
|
+
CI security scanner for Dockerfiles, dependencies, and secrets. Powered by Trivy.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
**Docker is required.** Trivy runs inside a container - no other installation needed. All major CI platforms (Bitbucket, GitHub Actions, GitLab CI, CircleCI) have Docker available.
|
|
8
|
+
|
|
9
|
+
## One-Line Usage
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Scan a Dockerfile
|
|
13
|
+
npx tc-scanner scan ./Dockerfile
|
|
14
|
+
|
|
15
|
+
# Scan dependencies (requires package-lock.json, yarn.lock, or pnpm-lock.yaml)
|
|
16
|
+
npx tc-scanner deps ./my-project
|
|
17
|
+
|
|
18
|
+
# Scan for secrets
|
|
19
|
+
npx tc-scanner secrets ./src
|
|
20
|
+
|
|
21
|
+
# Scan a container image
|
|
22
|
+
npx tc-scanner image nginx:latest
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Send Results to Webhook
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx tc-scanner scan ./Dockerfile --webhook https://your-webhook.com/endpoint
|
|
29
|
+
npx tc-scanner deps . --webhook https://hooks.slack.com/services/xxx
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Webhook payload:
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"scanner": "tc-scanner",
|
|
36
|
+
"scanType": "dockerfile",
|
|
37
|
+
"timestamp": "2024-01-15T10:30:00.000Z",
|
|
38
|
+
"summary": { "total": 2, "critical": 0, "high": 1, "medium": 1, "low": 0 },
|
|
39
|
+
"issues": [...]
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Bitbucket Pipelines
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
image: node:20
|
|
47
|
+
|
|
48
|
+
definitions:
|
|
49
|
+
services:
|
|
50
|
+
docker:
|
|
51
|
+
memory: 2048
|
|
52
|
+
|
|
53
|
+
pipelines:
|
|
54
|
+
default:
|
|
55
|
+
- step:
|
|
56
|
+
name: Security Scan
|
|
57
|
+
services:
|
|
58
|
+
- docker
|
|
59
|
+
script:
|
|
60
|
+
- npx tc-scanner scan ./Dockerfile --severity HIGH
|
|
61
|
+
- npx tc-scanner deps . --severity HIGH
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## GitHub Actions
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
jobs:
|
|
68
|
+
security:
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/checkout@v4
|
|
72
|
+
- name: Security Scan
|
|
73
|
+
run: |
|
|
74
|
+
npx tc-scanner scan ./Dockerfile --severity HIGH
|
|
75
|
+
npx tc-scanner deps . --severity HIGH
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## GitLab CI
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
security-scan:
|
|
82
|
+
image: node:20
|
|
83
|
+
services:
|
|
84
|
+
- docker:dind
|
|
85
|
+
script:
|
|
86
|
+
- npx tc-scanner scan ./Dockerfile --severity HIGH
|
|
87
|
+
- npx tc-scanner deps . --severity HIGH
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## CLI Options
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
Usage: tc-scan [command] [options]
|
|
94
|
+
|
|
95
|
+
Commands:
|
|
96
|
+
scan [path] Scan a Dockerfile
|
|
97
|
+
deps [path] Scan project dependencies
|
|
98
|
+
secrets [path] Scan for secrets in code
|
|
99
|
+
image <name> Scan a container image
|
|
100
|
+
|
|
101
|
+
Options:
|
|
102
|
+
-s, --severity Minimum severity: LOW, MEDIUM, HIGH, CRITICAL (default: HIGH)
|
|
103
|
+
-o, --output Save report to file (generates .json, .txt, .html)
|
|
104
|
+
--exit-code Exit code when issues found (default: 1)
|
|
105
|
+
--include-dev Include dev dependencies in scan
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Generate Reports
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Generates scan-report.json, scan-report.txt, scan-report.html
|
|
112
|
+
npx tc-scanner scan ./Dockerfile --output ./reports/scan-report
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Reports can be:
|
|
116
|
+
- Attached to CI artifacts
|
|
117
|
+
- Sent via email
|
|
118
|
+
- Posted to Slack/webhooks
|
|
119
|
+
|
|
120
|
+
## What It Detects
|
|
121
|
+
|
|
122
|
+
| Scan Type | Detects |
|
|
123
|
+
|-----------|---------|
|
|
124
|
+
| `scan` | Dockerfile misconfigurations (missing HEALTHCHECK, running as root, etc.) |
|
|
125
|
+
| `deps` | Known CVEs in npm/yarn/pnpm packages |
|
|
126
|
+
| `secrets` | API keys, tokens, passwords, private keys in code |
|
|
127
|
+
| `image` | OS package vulnerabilities + app dependencies in containers |
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { defineCommand, runMain } from 'citty';
|
|
4
|
+
import { scanDockerfile, scanImage, scanFilesystem, sendWebhook } from '../src/scanner.js';
|
|
5
|
+
|
|
6
|
+
const main = defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: 'tc-scan',
|
|
9
|
+
version: '0.1.0',
|
|
10
|
+
description: 'CI security scanner for Dockerfiles, dependencies, and secrets. Requires Docker.',
|
|
11
|
+
},
|
|
12
|
+
subCommands: {
|
|
13
|
+
scan: defineCommand({
|
|
14
|
+
meta: { description: 'Scan a Dockerfile for misconfigurations' },
|
|
15
|
+
args: {
|
|
16
|
+
path: { type: 'positional', description: 'Path to Dockerfile', default: './Dockerfile' },
|
|
17
|
+
severity: { type: 'string', alias: 's', description: 'Minimum severity (LOW, MEDIUM, HIGH, CRITICAL)', default: 'HIGH' },
|
|
18
|
+
output: { type: 'string', alias: 'o', description: 'Save report to file (generates .json, .txt, .html)' },
|
|
19
|
+
webhook: { type: 'string', alias: 'w', description: 'Send results to webhook URL' },
|
|
20
|
+
'exit-code': { type: 'string', description: 'Exit code when issues found', default: '1' },
|
|
21
|
+
},
|
|
22
|
+
async run({ args }) {
|
|
23
|
+
console.log('━'.repeat(50));
|
|
24
|
+
console.log(' TC-Secure Scanner');
|
|
25
|
+
console.log('━'.repeat(50));
|
|
26
|
+
console.log();
|
|
27
|
+
|
|
28
|
+
const result = await scanDockerfile(args.path, {
|
|
29
|
+
severity: args.severity,
|
|
30
|
+
output: args.output,
|
|
31
|
+
exitCode: args['exit-code'],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (args.webhook && result.json) {
|
|
35
|
+
await sendWebhook(args.webhook, result.json, 'dockerfile');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
process.exit(result.exitCode);
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
|
|
42
|
+
image: defineCommand({
|
|
43
|
+
meta: { description: 'Scan a container image for vulnerabilities' },
|
|
44
|
+
args: {
|
|
45
|
+
image: { type: 'positional', description: 'Image name (e.g., nginx:latest)', required: true },
|
|
46
|
+
severity: { type: 'string', alias: 's', description: 'Minimum severity', default: 'HIGH' },
|
|
47
|
+
output: { type: 'string', alias: 'o', description: 'Save report to file' },
|
|
48
|
+
webhook: { type: 'string', alias: 'w', description: 'Send results to webhook URL' },
|
|
49
|
+
'exit-code': { type: 'string', description: 'Exit code when issues found', default: '1' },
|
|
50
|
+
},
|
|
51
|
+
async run({ args }) {
|
|
52
|
+
console.log('━'.repeat(50));
|
|
53
|
+
console.log(' TC-Secure Image Scanner');
|
|
54
|
+
console.log('━'.repeat(50));
|
|
55
|
+
console.log();
|
|
56
|
+
|
|
57
|
+
const result = await scanImage(args.image, {
|
|
58
|
+
severity: args.severity,
|
|
59
|
+
output: args.output,
|
|
60
|
+
exitCode: args['exit-code'],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (args.webhook && result.json) {
|
|
64
|
+
await sendWebhook(args.webhook, result.json, 'image');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
process.exit(result.exitCode);
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
|
|
71
|
+
deps: defineCommand({
|
|
72
|
+
meta: { description: 'Scan project dependencies (package-lock.json, yarn.lock, etc.)' },
|
|
73
|
+
args: {
|
|
74
|
+
path: { type: 'positional', description: 'Path to project directory', default: '.' },
|
|
75
|
+
severity: { type: 'string', alias: 's', description: 'Minimum severity', default: 'HIGH' },
|
|
76
|
+
output: { type: 'string', alias: 'o', description: 'Save report to file' },
|
|
77
|
+
webhook: { type: 'string', alias: 'w', description: 'Send results to webhook URL' },
|
|
78
|
+
'exit-code': { type: 'string', description: 'Exit code when issues found', default: '1' },
|
|
79
|
+
'include-dev': { type: 'boolean', description: 'Include dev dependencies', default: false },
|
|
80
|
+
},
|
|
81
|
+
async run({ args }) {
|
|
82
|
+
console.log('━'.repeat(50));
|
|
83
|
+
console.log(' TC-Secure Dependency Scanner');
|
|
84
|
+
console.log('━'.repeat(50));
|
|
85
|
+
console.log();
|
|
86
|
+
|
|
87
|
+
const result = await scanFilesystem(args.path, {
|
|
88
|
+
severity: args.severity,
|
|
89
|
+
output: args.output,
|
|
90
|
+
exitCode: args['exit-code'],
|
|
91
|
+
includeDev: args['include-dev'],
|
|
92
|
+
scanType: 'vuln',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (args.webhook && result.json) {
|
|
96
|
+
await sendWebhook(args.webhook, result.json, 'dependencies');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
process.exit(result.exitCode);
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
|
|
103
|
+
secrets: defineCommand({
|
|
104
|
+
meta: { description: 'Scan for secrets and sensitive data in code' },
|
|
105
|
+
args: {
|
|
106
|
+
path: { type: 'positional', description: 'Path to project directory', default: '.' },
|
|
107
|
+
output: { type: 'string', alias: 'o', description: 'Save report to file' },
|
|
108
|
+
webhook: { type: 'string', alias: 'w', description: 'Send results to webhook URL' },
|
|
109
|
+
'exit-code': { type: 'string', description: 'Exit code when secrets found', default: '1' },
|
|
110
|
+
},
|
|
111
|
+
async run({ args }) {
|
|
112
|
+
console.log('━'.repeat(50));
|
|
113
|
+
console.log(' TC-Secure Secrets Scanner');
|
|
114
|
+
console.log('━'.repeat(50));
|
|
115
|
+
console.log();
|
|
116
|
+
|
|
117
|
+
const result = await scanFilesystem(args.path, {
|
|
118
|
+
output: args.output,
|
|
119
|
+
exitCode: args['exit-code'],
|
|
120
|
+
scanType: 'secret',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (args.webhook && result.json) {
|
|
124
|
+
await sendWebhook(args.webhook, result.json, 'secrets');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
process.exit(result.exitCode);
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
runMain(main);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Example Bitbucket Pipelines configuration for tc-scan
|
|
2
|
+
# Copy this to your project's bitbucket-pipelines.yml
|
|
3
|
+
|
|
4
|
+
image: node:20
|
|
5
|
+
|
|
6
|
+
definitions:
|
|
7
|
+
services:
|
|
8
|
+
docker:
|
|
9
|
+
memory: 2048
|
|
10
|
+
|
|
11
|
+
pipelines:
|
|
12
|
+
default:
|
|
13
|
+
- step:
|
|
14
|
+
name: Security Scan
|
|
15
|
+
services:
|
|
16
|
+
- docker
|
|
17
|
+
caches:
|
|
18
|
+
- node
|
|
19
|
+
- docker
|
|
20
|
+
script:
|
|
21
|
+
# Install scanner dependencies
|
|
22
|
+
- cd ci-scanner && npm install
|
|
23
|
+
|
|
24
|
+
# Scan Dockerfile
|
|
25
|
+
- npm run scan -- ../pentest/Dockerfile --severity HIGH
|
|
26
|
+
|
|
27
|
+
# Optionally scan the built image
|
|
28
|
+
# - npm run scan image your-image:latest
|
|
29
|
+
|
|
30
|
+
pull-requests:
|
|
31
|
+
'**':
|
|
32
|
+
- step:
|
|
33
|
+
name: PR Security Scan
|
|
34
|
+
services:
|
|
35
|
+
- docker
|
|
36
|
+
script:
|
|
37
|
+
- cd ci-scanner && npm install
|
|
38
|
+
- node bin/cli.js scan ../pentest/Dockerfile --severity MEDIUM
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tc-scanner",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CI security scanner for Dockerfiles, dependencies, and secrets using Trivy",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tc-scan": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"scan": "node bin/cli.js scan",
|
|
11
|
+
"test": "node bin/cli.js scan ./samples/Dockerfile"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"security",
|
|
15
|
+
"scanner",
|
|
16
|
+
"trivy",
|
|
17
|
+
"docker",
|
|
18
|
+
"dockerfile",
|
|
19
|
+
"vulnerability",
|
|
20
|
+
"ci",
|
|
21
|
+
"devops"
|
|
22
|
+
],
|
|
23
|
+
"author": "TC-Secure",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/aspect-software-co/tc-scanner.git"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"citty": "^0.1.6"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html>
|
|
4
|
+
<head>
|
|
5
|
+
<style>
|
|
6
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
|
|
7
|
+
.header { background: #1e293b; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
8
|
+
.content { border: 1px solid #e2e8f0; border-top: none; padding: 20px; border-radius: 0 0 8px 8px; }
|
|
9
|
+
.summary-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin: 20px 0; }
|
|
10
|
+
.stat { text-align: center; padding: 15px; border-radius: 8px; }
|
|
11
|
+
.stat-critical { background: #fef2f2; border: 1px solid #fecaca; }
|
|
12
|
+
.stat-high { background: #fff7ed; border: 1px solid #fed7aa; }
|
|
13
|
+
.stat-medium { background: #fefce8; border: 1px solid #fef08a; }
|
|
14
|
+
.stat-low { background: #eff6ff; border: 1px solid #bfdbfe; }
|
|
15
|
+
.stat-value { font-size: 24px; font-weight: bold; }
|
|
16
|
+
.issue { border-left: 4px solid; padding: 12px; margin: 10px 0; background: #f8fafc; }
|
|
17
|
+
.issue-critical { border-color: #dc2626; }
|
|
18
|
+
.issue-high { border-color: #ea580c; }
|
|
19
|
+
.issue-medium { border-color: #ca8a04; }
|
|
20
|
+
.issue-low { border-color: #2563eb; }
|
|
21
|
+
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; color: white; }
|
|
22
|
+
a { color: #2563eb; }
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<div class="header">
|
|
27
|
+
<h1 style="margin: 0;">TC-Secure Scan Report</h1>
|
|
28
|
+
<p style="margin: 5px 0 0 0; opacity: 0.8;">Target: /Users/themba/Teamcoda/teamcoda/tc-secure/pentest/Dockerfile</p>
|
|
29
|
+
<p style="margin: 5px 0 0 0; opacity: 0.8;">Scan Time: 2026-02-02T00:06:36.733Z</p>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="content">
|
|
32
|
+
<h2>Summary</h2>
|
|
33
|
+
<div class="summary-grid">
|
|
34
|
+
<div class="stat stat-critical">
|
|
35
|
+
<div class="stat-value" style="color: #dc2626">0</div>
|
|
36
|
+
<div>Critical</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="stat stat-high">
|
|
39
|
+
<div class="stat-value" style="color: #ea580c">0</div>
|
|
40
|
+
<div>High</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="stat stat-medium">
|
|
43
|
+
<div class="stat-value" style="color: #ca8a04">0</div>
|
|
44
|
+
<div>Medium</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="stat stat-low">
|
|
47
|
+
<div class="stat-value" style="color: #2563eb">1</div>
|
|
48
|
+
<div>Low</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<h2>Issues (1)</h2>
|
|
52
|
+
<div class="issue issue-low">
|
|
53
|
+
<span class="badge" style="background: #2563eb">LOW</span>
|
|
54
|
+
<strong>DS-0026</strong>
|
|
55
|
+
<p style="margin: 8px 0;">No HEALTHCHECK defined</p>
|
|
56
|
+
|
|
57
|
+
<a href="https://avd.aquasec.com/misconfig/ds-0026" style="font-size: 14px;">View details →</a>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</body>
|
|
61
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"target": "/Users/themba/Teamcoda/teamcoda/tc-secure/pentest/Dockerfile",
|
|
3
|
+
"scanTime": "2026-02-02T00:06:36.733Z",
|
|
4
|
+
"totalIssues": 1,
|
|
5
|
+
"bySeverity": {
|
|
6
|
+
"CRITICAL": 0,
|
|
7
|
+
"HIGH": 0,
|
|
8
|
+
"MEDIUM": 0,
|
|
9
|
+
"LOW": 1
|
|
10
|
+
},
|
|
11
|
+
"issues": [
|
|
12
|
+
{
|
|
13
|
+
"id": "DS-0026",
|
|
14
|
+
"severity": "LOW",
|
|
15
|
+
"title": "No HEALTHCHECK defined",
|
|
16
|
+
"description": "You should add HEALTHCHECK instruction in your docker container images to perform the health check on running containers.",
|
|
17
|
+
"resolution": "Add HEALTHCHECK instruction in Dockerfile",
|
|
18
|
+
"url": "https://avd.aquasec.com/misconfig/ds-0026"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
TC-SECURE SCAN REPORT
|
|
3
|
+
=====================
|
|
4
|
+
Target: /Users/themba/Teamcoda/teamcoda/tc-secure/pentest/Dockerfile
|
|
5
|
+
Scan Time: 2026-02-02T00:06:36.733Z
|
|
6
|
+
|
|
7
|
+
SUMMARY
|
|
8
|
+
-------
|
|
9
|
+
Total Issues: 1
|
|
10
|
+
CRITICAL: 0
|
|
11
|
+
HIGH: 0
|
|
12
|
+
MEDIUM: 0
|
|
13
|
+
LOW: 1
|
|
14
|
+
|
|
15
|
+
DETAILS
|
|
16
|
+
-------
|
|
17
|
+
|
|
18
|
+
[LOW] DS-0026
|
|
19
|
+
No HEALTHCHECK defined
|
|
20
|
+
More info: https://avd.aquasec.com/misconfig/ds-0026
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Sample Dockerfile for testing the scanner
|
|
2
|
+
FROM node:20-alpine
|
|
3
|
+
|
|
4
|
+
# Intentional misconfiguration: running as root
|
|
5
|
+
USER root
|
|
6
|
+
|
|
7
|
+
WORKDIR /app
|
|
8
|
+
|
|
9
|
+
COPY package*.json ./
|
|
10
|
+
RUN npm install
|
|
11
|
+
|
|
12
|
+
COPY . .
|
|
13
|
+
|
|
14
|
+
# Intentional: exposing common port
|
|
15
|
+
EXPOSE 3000
|
|
16
|
+
|
|
17
|
+
CMD ["node", "server.js"]
|
package/src/scanner.js
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Execute a command and return the result
|
|
7
|
+
*/
|
|
8
|
+
function exec(command, args, options = {}) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const silent = options.silent || false;
|
|
11
|
+
const proc = spawn(command, args, {
|
|
12
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
let stdout = '';
|
|
16
|
+
let stderr = '';
|
|
17
|
+
|
|
18
|
+
proc.stdout.on('data', (data) => {
|
|
19
|
+
stdout += data.toString();
|
|
20
|
+
if (!silent) {
|
|
21
|
+
process.stdout.write(data);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
proc.stderr.on('data', (data) => {
|
|
26
|
+
stderr += data.toString();
|
|
27
|
+
if (!silent) {
|
|
28
|
+
process.stderr.write(data);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
proc.on('close', (code) => {
|
|
33
|
+
resolve({ code, stdout, stderr });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
proc.on('error', (err) => {
|
|
37
|
+
reject(err);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate a summary from JSON results
|
|
44
|
+
*/
|
|
45
|
+
export function generateSummary(jsonData, targetName) {
|
|
46
|
+
const results = JSON.parse(jsonData);
|
|
47
|
+
|
|
48
|
+
let summary = {
|
|
49
|
+
target: targetName,
|
|
50
|
+
scanTime: new Date().toISOString(),
|
|
51
|
+
totalIssues: 0,
|
|
52
|
+
bySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 },
|
|
53
|
+
issues: [],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (results.Results) {
|
|
57
|
+
for (const result of results.Results) {
|
|
58
|
+
if (result.Misconfigurations) {
|
|
59
|
+
for (const issue of result.Misconfigurations) {
|
|
60
|
+
summary.totalIssues++;
|
|
61
|
+
summary.bySeverity[issue.Severity] = (summary.bySeverity[issue.Severity] || 0) + 1;
|
|
62
|
+
summary.issues.push({
|
|
63
|
+
id: issue.ID,
|
|
64
|
+
severity: issue.Severity,
|
|
65
|
+
title: issue.Title,
|
|
66
|
+
description: issue.Description,
|
|
67
|
+
resolution: issue.Resolution,
|
|
68
|
+
url: issue.PrimaryURL,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (result.Vulnerabilities) {
|
|
73
|
+
for (const vuln of result.Vulnerabilities) {
|
|
74
|
+
summary.totalIssues++;
|
|
75
|
+
summary.bySeverity[vuln.Severity] = (summary.bySeverity[vuln.Severity] || 0) + 1;
|
|
76
|
+
summary.issues.push({
|
|
77
|
+
id: vuln.VulnerabilityID,
|
|
78
|
+
severity: vuln.Severity,
|
|
79
|
+
title: vuln.Title || vuln.VulnerabilityID,
|
|
80
|
+
package: vuln.PkgName,
|
|
81
|
+
installedVersion: vuln.InstalledVersion,
|
|
82
|
+
fixedVersion: vuln.FixedVersion,
|
|
83
|
+
url: vuln.PrimaryURL,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return summary;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Format summary as plain text
|
|
95
|
+
*/
|
|
96
|
+
export function formatAsText(summary) {
|
|
97
|
+
let text = `
|
|
98
|
+
TC-SECURE SCAN REPORT
|
|
99
|
+
=====================
|
|
100
|
+
Target: ${summary.target}
|
|
101
|
+
Scan Time: ${summary.scanTime}
|
|
102
|
+
|
|
103
|
+
SUMMARY
|
|
104
|
+
-------
|
|
105
|
+
Total Issues: ${summary.totalIssues}
|
|
106
|
+
CRITICAL: ${summary.bySeverity.CRITICAL}
|
|
107
|
+
HIGH: ${summary.bySeverity.HIGH}
|
|
108
|
+
MEDIUM: ${summary.bySeverity.MEDIUM}
|
|
109
|
+
LOW: ${summary.bySeverity.LOW}
|
|
110
|
+
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
if (summary.issues.length > 0) {
|
|
114
|
+
text += `DETAILS\n-------\n`;
|
|
115
|
+
for (const issue of summary.issues) {
|
|
116
|
+
text += `\n[${issue.severity}] ${issue.id}\n`;
|
|
117
|
+
text += ` ${issue.title}\n`;
|
|
118
|
+
if (issue.package) {
|
|
119
|
+
text += ` Package: ${issue.package} (${issue.installedVersion})\n`;
|
|
120
|
+
if (issue.fixedVersion) {
|
|
121
|
+
text += ` Fix: Upgrade to ${issue.fixedVersion}\n`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (issue.url) {
|
|
125
|
+
text += ` More info: ${issue.url}\n`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
text += `No issues found.\n`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return text;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Format summary as HTML
|
|
137
|
+
*/
|
|
138
|
+
export function formatAsHtml(summary) {
|
|
139
|
+
const severityColors = {
|
|
140
|
+
CRITICAL: '#dc2626',
|
|
141
|
+
HIGH: '#ea580c',
|
|
142
|
+
MEDIUM: '#ca8a04',
|
|
143
|
+
LOW: '#2563eb',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
let html = `<!DOCTYPE html>
|
|
147
|
+
<html>
|
|
148
|
+
<head>
|
|
149
|
+
<style>
|
|
150
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
|
|
151
|
+
.header { background: #1e293b; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
152
|
+
.content { border: 1px solid #e2e8f0; border-top: none; padding: 20px; border-radius: 0 0 8px 8px; }
|
|
153
|
+
.summary-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin: 20px 0; }
|
|
154
|
+
.stat { text-align: center; padding: 15px; border-radius: 8px; }
|
|
155
|
+
.stat-critical { background: #fef2f2; border: 1px solid #fecaca; }
|
|
156
|
+
.stat-high { background: #fff7ed; border: 1px solid #fed7aa; }
|
|
157
|
+
.stat-medium { background: #fefce8; border: 1px solid #fef08a; }
|
|
158
|
+
.stat-low { background: #eff6ff; border: 1px solid #bfdbfe; }
|
|
159
|
+
.stat-value { font-size: 24px; font-weight: bold; }
|
|
160
|
+
.issue { border-left: 4px solid; padding: 12px; margin: 10px 0; background: #f8fafc; }
|
|
161
|
+
.issue-critical { border-color: #dc2626; }
|
|
162
|
+
.issue-high { border-color: #ea580c; }
|
|
163
|
+
.issue-medium { border-color: #ca8a04; }
|
|
164
|
+
.issue-low { border-color: #2563eb; }
|
|
165
|
+
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; color: white; }
|
|
166
|
+
a { color: #2563eb; }
|
|
167
|
+
</style>
|
|
168
|
+
</head>
|
|
169
|
+
<body>
|
|
170
|
+
<div class="header">
|
|
171
|
+
<h1 style="margin: 0;">TC-Secure Scan Report</h1>
|
|
172
|
+
<p style="margin: 5px 0 0 0; opacity: 0.8;">Target: ${summary.target}</p>
|
|
173
|
+
<p style="margin: 5px 0 0 0; opacity: 0.8;">Scan Time: ${summary.scanTime}</p>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="content">
|
|
176
|
+
<h2>Summary</h2>
|
|
177
|
+
<div class="summary-grid">
|
|
178
|
+
<div class="stat stat-critical">
|
|
179
|
+
<div class="stat-value" style="color: ${severityColors.CRITICAL}">${summary.bySeverity.CRITICAL}</div>
|
|
180
|
+
<div>Critical</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="stat stat-high">
|
|
183
|
+
<div class="stat-value" style="color: ${severityColors.HIGH}">${summary.bySeverity.HIGH}</div>
|
|
184
|
+
<div>High</div>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="stat stat-medium">
|
|
187
|
+
<div class="stat-value" style="color: ${severityColors.MEDIUM}">${summary.bySeverity.MEDIUM}</div>
|
|
188
|
+
<div>Medium</div>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="stat stat-low">
|
|
191
|
+
<div class="stat-value" style="color: ${severityColors.LOW}">${summary.bySeverity.LOW}</div>
|
|
192
|
+
<div>Low</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
`;
|
|
196
|
+
|
|
197
|
+
if (summary.issues.length > 0) {
|
|
198
|
+
html += `<h2>Issues (${summary.totalIssues})</h2>`;
|
|
199
|
+
for (const issue of summary.issues) {
|
|
200
|
+
const severityClass = issue.severity.toLowerCase();
|
|
201
|
+
html += `
|
|
202
|
+
<div class="issue issue-${severityClass}">
|
|
203
|
+
<span class="badge" style="background: ${severityColors[issue.severity]}">${issue.severity}</span>
|
|
204
|
+
<strong>${issue.id}</strong>
|
|
205
|
+
<p style="margin: 8px 0;">${issue.title}</p>
|
|
206
|
+
${issue.package ? `<p style="margin: 4px 0; font-size: 14px; color: #64748b;">Package: ${issue.package} (${issue.installedVersion})${issue.fixedVersion ? ` → ${issue.fixedVersion}` : ''}</p>` : ''}
|
|
207
|
+
${issue.url ? `<a href="${issue.url}" style="font-size: 14px;">View details →</a>` : ''}
|
|
208
|
+
</div>`;
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
html += `<p style="color: #16a34a; font-weight: 500;">✓ No security issues found.</p>`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
html += `
|
|
215
|
+
</div>
|
|
216
|
+
</body>
|
|
217
|
+
</html>`;
|
|
218
|
+
|
|
219
|
+
return html;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Save report to file
|
|
224
|
+
*/
|
|
225
|
+
export function saveReport(content, filePath) {
|
|
226
|
+
const dir = dirname(filePath);
|
|
227
|
+
if (!existsSync(dir)) {
|
|
228
|
+
mkdirSync(dir, { recursive: true });
|
|
229
|
+
}
|
|
230
|
+
writeFileSync(filePath, content);
|
|
231
|
+
return filePath;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Send results to webhook
|
|
236
|
+
*/
|
|
237
|
+
export async function sendWebhook(url, jsonData, scanType) {
|
|
238
|
+
const summary = generateSummary(jsonData, scanType);
|
|
239
|
+
|
|
240
|
+
const payload = {
|
|
241
|
+
scanner: 'tc-scanner',
|
|
242
|
+
scanType,
|
|
243
|
+
timestamp: summary.scanTime,
|
|
244
|
+
target: summary.target,
|
|
245
|
+
summary: {
|
|
246
|
+
total: summary.totalIssues,
|
|
247
|
+
critical: summary.bySeverity.CRITICAL,
|
|
248
|
+
high: summary.bySeverity.HIGH,
|
|
249
|
+
medium: summary.bySeverity.MEDIUM,
|
|
250
|
+
low: summary.bySeverity.LOW,
|
|
251
|
+
},
|
|
252
|
+
issues: summary.issues,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
console.log(`\nSending results to webhook: ${url}`);
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const response = await fetch(url, {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: { 'Content-Type': 'application/json' },
|
|
261
|
+
body: JSON.stringify(payload),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (response.ok) {
|
|
265
|
+
console.log(`Webhook sent successfully (${response.status})`);
|
|
266
|
+
} else {
|
|
267
|
+
console.error(`Webhook failed: ${response.status} ${response.statusText}`);
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error(`Webhook error: ${error.message}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check if Docker is available
|
|
276
|
+
*/
|
|
277
|
+
async function checkDocker() {
|
|
278
|
+
try {
|
|
279
|
+
const result = await exec('docker', ['--version']);
|
|
280
|
+
return result.code === 0;
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Scan a Dockerfile for misconfigurations
|
|
288
|
+
*/
|
|
289
|
+
export async function scanDockerfile(path, options) {
|
|
290
|
+
const resolvedPath = resolve(process.cwd(), path);
|
|
291
|
+
|
|
292
|
+
if (!existsSync(resolvedPath)) {
|
|
293
|
+
throw new Error(`File not found: ${resolvedPath}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const dockerAvailable = await checkDocker();
|
|
297
|
+
if (!dockerAvailable) {
|
|
298
|
+
throw new Error('Docker is required. Install Docker to use this scanner.');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.log(`Scanning: ${resolvedPath}`);
|
|
302
|
+
console.log(`Severity: ${options.severity}+`);
|
|
303
|
+
console.log();
|
|
304
|
+
|
|
305
|
+
const mountPath = dirname(resolvedPath);
|
|
306
|
+
const fileName = resolvedPath.split('/').pop();
|
|
307
|
+
|
|
308
|
+
// Run with JSON format to get structured data
|
|
309
|
+
const jsonArgs = [
|
|
310
|
+
'run', '--rm',
|
|
311
|
+
'-v', `${mountPath}:/src:ro`,
|
|
312
|
+
'aquasec/trivy',
|
|
313
|
+
'config',
|
|
314
|
+
`/src/${fileName}`,
|
|
315
|
+
'--severity', options.severity,
|
|
316
|
+
'--format', 'json',
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
const jsonResult = await exec('docker', jsonArgs, { silent: true });
|
|
320
|
+
|
|
321
|
+
// Run with table format for display
|
|
322
|
+
const tableArgs = [
|
|
323
|
+
'run', '--rm',
|
|
324
|
+
'-v', `${mountPath}:/src:ro`,
|
|
325
|
+
'aquasec/trivy',
|
|
326
|
+
'config',
|
|
327
|
+
`/src/${fileName}`,
|
|
328
|
+
'--severity', options.severity,
|
|
329
|
+
'--format', 'table',
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
if (options.exitCode !== '0') {
|
|
333
|
+
tableArgs.push('--exit-code', options.exitCode);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const tableResult = await exec('docker', tableArgs);
|
|
337
|
+
|
|
338
|
+
// Generate reports if output option is set
|
|
339
|
+
let reportPaths = {};
|
|
340
|
+
if (options.output && jsonResult.stdout) {
|
|
341
|
+
const summary = generateSummary(jsonResult.stdout, resolvedPath);
|
|
342
|
+
const basePath = options.output.replace(/\.[^/.]+$/, '');
|
|
343
|
+
|
|
344
|
+
reportPaths.json = saveReport(JSON.stringify(summary, null, 2), `${basePath}.json`);
|
|
345
|
+
reportPaths.text = saveReport(formatAsText(summary), `${basePath}.txt`);
|
|
346
|
+
reportPaths.html = saveReport(formatAsHtml(summary), `${basePath}.html`);
|
|
347
|
+
|
|
348
|
+
console.log();
|
|
349
|
+
console.log('Reports saved:');
|
|
350
|
+
console.log(` JSON: ${reportPaths.json}`);
|
|
351
|
+
console.log(` Text: ${reportPaths.text}`);
|
|
352
|
+
console.log(` HTML: ${reportPaths.html}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
exitCode: tableResult.code,
|
|
357
|
+
output: tableResult.stdout,
|
|
358
|
+
json: jsonResult.stdout,
|
|
359
|
+
reportPaths,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Scan a container image for vulnerabilities
|
|
365
|
+
*/
|
|
366
|
+
export async function scanImage(image, options) {
|
|
367
|
+
const dockerAvailable = await checkDocker();
|
|
368
|
+
if (!dockerAvailable) {
|
|
369
|
+
throw new Error('Docker is required. Install Docker to use this scanner.');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
console.log(`Scanning image: ${image}`);
|
|
373
|
+
console.log(`Severity: ${options.severity}+`);
|
|
374
|
+
console.log();
|
|
375
|
+
|
|
376
|
+
// Run with JSON format
|
|
377
|
+
const jsonArgs = [
|
|
378
|
+
'run', '--rm',
|
|
379
|
+
'-v', '/var/run/docker.sock:/var/run/docker.sock',
|
|
380
|
+
'aquasec/trivy',
|
|
381
|
+
'image',
|
|
382
|
+
image,
|
|
383
|
+
'--severity', options.severity,
|
|
384
|
+
'--format', 'json',
|
|
385
|
+
];
|
|
386
|
+
|
|
387
|
+
const jsonResult = await exec('docker', jsonArgs, { silent: true });
|
|
388
|
+
|
|
389
|
+
// Run with table format for display
|
|
390
|
+
const tableArgs = [
|
|
391
|
+
'run', '--rm',
|
|
392
|
+
'-v', '/var/run/docker.sock:/var/run/docker.sock',
|
|
393
|
+
'aquasec/trivy',
|
|
394
|
+
'image',
|
|
395
|
+
image,
|
|
396
|
+
'--severity', options.severity,
|
|
397
|
+
'--format', 'table',
|
|
398
|
+
];
|
|
399
|
+
|
|
400
|
+
if (options.exitCode !== '0') {
|
|
401
|
+
tableArgs.push('--exit-code', options.exitCode);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const tableResult = await exec('docker', tableArgs);
|
|
405
|
+
|
|
406
|
+
// Generate reports
|
|
407
|
+
let reportPaths = {};
|
|
408
|
+
if (options.output && jsonResult.stdout) {
|
|
409
|
+
const summary = generateSummary(jsonResult.stdout, image);
|
|
410
|
+
const basePath = options.output.replace(/\.[^/.]+$/, '');
|
|
411
|
+
|
|
412
|
+
reportPaths.json = saveReport(JSON.stringify(summary, null, 2), `${basePath}.json`);
|
|
413
|
+
reportPaths.text = saveReport(formatAsText(summary), `${basePath}.txt`);
|
|
414
|
+
reportPaths.html = saveReport(formatAsHtml(summary), `${basePath}.html`);
|
|
415
|
+
|
|
416
|
+
console.log();
|
|
417
|
+
console.log('Reports saved:');
|
|
418
|
+
console.log(` JSON: ${reportPaths.json}`);
|
|
419
|
+
console.log(` Text: ${reportPaths.text}`);
|
|
420
|
+
console.log(` HTML: ${reportPaths.html}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
exitCode: tableResult.code,
|
|
425
|
+
output: tableResult.stdout,
|
|
426
|
+
json: jsonResult.stdout,
|
|
427
|
+
reportPaths,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Scan a filesystem/project for vulnerabilities or secrets
|
|
433
|
+
*/
|
|
434
|
+
export async function scanFilesystem(path, options) {
|
|
435
|
+
const resolvedPath = resolve(process.cwd(), path);
|
|
436
|
+
|
|
437
|
+
if (!existsSync(resolvedPath)) {
|
|
438
|
+
throw new Error(`Directory not found: ${resolvedPath}`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const dockerAvailable = await checkDocker();
|
|
442
|
+
if (!dockerAvailable) {
|
|
443
|
+
throw new Error('Docker is required. Install Docker to use this scanner.');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const scanType = options.scanType || 'vuln';
|
|
447
|
+
const scanners = scanType === 'secret' ? 'secret' : 'vuln';
|
|
448
|
+
|
|
449
|
+
console.log(`Scanning: ${resolvedPath}`);
|
|
450
|
+
console.log(`Scan type: ${scanners}`);
|
|
451
|
+
if (scanType !== 'secret') {
|
|
452
|
+
console.log(`Severity: ${options.severity}+`);
|
|
453
|
+
}
|
|
454
|
+
console.log();
|
|
455
|
+
|
|
456
|
+
// Run with JSON format
|
|
457
|
+
const jsonArgs = [
|
|
458
|
+
'run', '--rm',
|
|
459
|
+
'-v', `${resolvedPath}:/src:ro`,
|
|
460
|
+
'aquasec/trivy',
|
|
461
|
+
'fs',
|
|
462
|
+
'/src',
|
|
463
|
+
'--scanners', scanners,
|
|
464
|
+
'--format', 'json',
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
if (scanType !== 'secret' && options.severity) {
|
|
468
|
+
jsonArgs.push('--severity', options.severity);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (options.includeDev) {
|
|
472
|
+
jsonArgs.push('--include-dev-deps');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const jsonResult = await exec('docker', jsonArgs, { silent: true });
|
|
476
|
+
|
|
477
|
+
// Run with table format for display
|
|
478
|
+
const tableArgs = [
|
|
479
|
+
'run', '--rm',
|
|
480
|
+
'-v', `${resolvedPath}:/src:ro`,
|
|
481
|
+
'aquasec/trivy',
|
|
482
|
+
'fs',
|
|
483
|
+
'/src',
|
|
484
|
+
'--scanners', scanners,
|
|
485
|
+
'--format', 'table',
|
|
486
|
+
];
|
|
487
|
+
|
|
488
|
+
if (scanType !== 'secret' && options.severity) {
|
|
489
|
+
tableArgs.push('--severity', options.severity);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (options.includeDev) {
|
|
493
|
+
tableArgs.push('--include-dev-deps');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (options.exitCode !== '0') {
|
|
497
|
+
tableArgs.push('--exit-code', options.exitCode);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const tableResult = await exec('docker', tableArgs);
|
|
501
|
+
|
|
502
|
+
// Generate reports
|
|
503
|
+
let reportPaths = {};
|
|
504
|
+
if (options.output && jsonResult.stdout) {
|
|
505
|
+
const summary = generateSummary(jsonResult.stdout, resolvedPath);
|
|
506
|
+
const basePath = options.output.replace(/\.[^/.]+$/, '');
|
|
507
|
+
|
|
508
|
+
reportPaths.json = saveReport(JSON.stringify(summary, null, 2), `${basePath}.json`);
|
|
509
|
+
reportPaths.text = saveReport(formatAsText(summary), `${basePath}.txt`);
|
|
510
|
+
reportPaths.html = saveReport(formatAsHtml(summary), `${basePath}.html`);
|
|
511
|
+
|
|
512
|
+
console.log();
|
|
513
|
+
console.log('Reports saved:');
|
|
514
|
+
console.log(` JSON: ${reportPaths.json}`);
|
|
515
|
+
console.log(` Text: ${reportPaths.text}`);
|
|
516
|
+
console.log(` HTML: ${reportPaths.html}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
exitCode: tableResult.code,
|
|
521
|
+
output: tableResult.stdout,
|
|
522
|
+
json: jsonResult.stdout,
|
|
523
|
+
reportPaths,
|
|
524
|
+
};
|
|
525
|
+
}
|