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 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
+ }