ngx-security-audit 1.0.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/LICENSE +21 -0
- package/README.md +385 -0
- package/bin/ngx-security-audit.js +192 -0
- package/package.json +63 -0
- package/src/config.js +65 -0
- package/src/index.js +54 -0
- package/src/reporters/console-reporter.js +111 -0
- package/src/reporters/html-reporter.js +191 -0
- package/src/reporters/index.js +23 -0
- package/src/reporters/json-reporter.js +10 -0
- package/src/reporters/sarif-reporter.js +111 -0
- package/src/rules/angular-config-rules.js +280 -0
- package/src/rules/auth-rules.js +153 -0
- package/src/rules/best-practice-rules.js +283 -0
- package/src/rules/http-rules.js +163 -0
- package/src/rules/index.js +25 -0
- package/src/rules/injection-rules.js +134 -0
- package/src/rules/sensitive-data-rules.js +168 -0
- package/src/rules/xss-rules.js +162 -0
- package/src/scanner.js +154 -0
- package/src/utils/file-utils.js +138 -0
- package/src/utils/severity.js +86 -0
package/src/config.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CONFIG = {
|
|
7
|
+
// Severity threshold for exit code (critical, high, medium, low, info)
|
|
8
|
+
threshold: 'high',
|
|
9
|
+
// Output format (console, json, html, sarif)
|
|
10
|
+
format: 'console',
|
|
11
|
+
// Output file for report
|
|
12
|
+
output: null,
|
|
13
|
+
// Directories to scan (relative to project root)
|
|
14
|
+
include: ['src/**/*.ts', 'src/**/*.html', 'src/**/*.js'],
|
|
15
|
+
// Patterns to ignore
|
|
16
|
+
exclude: [],
|
|
17
|
+
// Rules to disable
|
|
18
|
+
disabledRules: [],
|
|
19
|
+
// Rules to treat as specific severity (overrides)
|
|
20
|
+
ruleOverrides: {},
|
|
21
|
+
// Verbose output
|
|
22
|
+
verbose: false,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load configuration from .ngsecurityrc.json or package.json
|
|
27
|
+
*/
|
|
28
|
+
function loadConfig(projectPath, cliOptions = {}) {
|
|
29
|
+
let fileConfig = {};
|
|
30
|
+
|
|
31
|
+
// Try .ngsecurityrc.json
|
|
32
|
+
const rcPath = path.join(projectPath, '.ngsecurityrc.json');
|
|
33
|
+
if (fs.existsSync(rcPath)) {
|
|
34
|
+
try {
|
|
35
|
+
const content = fs.readFileSync(rcPath, 'utf-8');
|
|
36
|
+
fileConfig = JSON.parse(content);
|
|
37
|
+
} catch {
|
|
38
|
+
// Ignore invalid config
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Try package.json "ngxSecurityAudit" key
|
|
43
|
+
if (Object.keys(fileConfig).length === 0) {
|
|
44
|
+
const pkgPath = path.join(projectPath, 'package.json');
|
|
45
|
+
if (fs.existsSync(pkgPath)) {
|
|
46
|
+
try {
|
|
47
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
48
|
+
if (pkg.ngxSecurityAudit) {
|
|
49
|
+
fileConfig = pkg.ngxSecurityAudit;
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Ignore
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Merge: defaults <- file config <- CLI options
|
|
58
|
+
return {
|
|
59
|
+
...DEFAULT_CONFIG,
|
|
60
|
+
...fileConfig,
|
|
61
|
+
...cliOptions,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { loadConfig, DEFAULT_CONFIG };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Scanner = require('./scanner');
|
|
4
|
+
const { getReporter } = require('./reporters');
|
|
5
|
+
const { SEVERITY, calculateScore, getGrade } = require('./utils/severity');
|
|
6
|
+
const allRules = require('./rules');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Run security audit on an Angular project
|
|
10
|
+
* @param {string} projectPath - Path to the Angular project root
|
|
11
|
+
* @param {object} options - Configuration options
|
|
12
|
+
* @returns {Promise<object>} Audit result
|
|
13
|
+
*/
|
|
14
|
+
async function audit(projectPath, options = {}) {
|
|
15
|
+
const scanner = new Scanner(projectPath, options);
|
|
16
|
+
return scanner.scan();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate formatted report from audit result
|
|
21
|
+
* @param {object} result - Audit result from audit()
|
|
22
|
+
* @param {string} format - Report format (console, json, html, sarif)
|
|
23
|
+
* @returns {string} Formatted report
|
|
24
|
+
*/
|
|
25
|
+
function generateReport(result, format = 'console') {
|
|
26
|
+
const reporter = getReporter(format);
|
|
27
|
+
return reporter(result);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* List all available security rules
|
|
32
|
+
* @returns {Array} List of rule objects
|
|
33
|
+
*/
|
|
34
|
+
function listRules() {
|
|
35
|
+
return allRules.map((rule) => ({
|
|
36
|
+
id: rule.id,
|
|
37
|
+
name: rule.name,
|
|
38
|
+
description: rule.description,
|
|
39
|
+
category: rule.category,
|
|
40
|
+
severity: rule.severity,
|
|
41
|
+
owasp: rule.owasp,
|
|
42
|
+
cwe: rule.cwe,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
audit,
|
|
48
|
+
generateReport,
|
|
49
|
+
listRules,
|
|
50
|
+
Scanner,
|
|
51
|
+
SEVERITY,
|
|
52
|
+
calculateScore,
|
|
53
|
+
getGrade,
|
|
54
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { SEVERITY_COLORS, SEVERITY_LABELS, SEVERITY } = require('../utils/severity');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Console Reporter - Beautiful terminal output with colors
|
|
8
|
+
*/
|
|
9
|
+
function consoleReport(result) {
|
|
10
|
+
const lines = [];
|
|
11
|
+
|
|
12
|
+
// Header
|
|
13
|
+
lines.push('');
|
|
14
|
+
lines.push(chalk.bold.cyan('╔══════════════════════════════════════════════════════════════╗'));
|
|
15
|
+
lines.push(chalk.bold.cyan('║') + chalk.bold.white(' NGX SECURITY AUDIT REPORT ') + chalk.bold.cyan('║'));
|
|
16
|
+
lines.push(chalk.bold.cyan('╚══════════════════════════════════════════════════════════════╝'));
|
|
17
|
+
lines.push('');
|
|
18
|
+
|
|
19
|
+
// Project info
|
|
20
|
+
lines.push(chalk.bold(' Project: ') + chalk.white(result.projectName || 'Unknown'));
|
|
21
|
+
lines.push(chalk.bold(' Angular: ') + chalk.white(result.angularVersion || 'Unknown'));
|
|
22
|
+
lines.push(chalk.bold(' Date: ') + chalk.white(new Date(result.scanDate).toLocaleString()));
|
|
23
|
+
lines.push(chalk.bold(' Files: ') + chalk.white(`${result.filesScanned} scanned`));
|
|
24
|
+
lines.push(chalk.bold(' Rules: ') + chalk.white(`${result.rulesExecuted} executed`));
|
|
25
|
+
lines.push('');
|
|
26
|
+
|
|
27
|
+
// Score
|
|
28
|
+
const scoreColor = result.score >= 80 ? 'green' : result.score >= 60 ? 'yellow' : 'red';
|
|
29
|
+
lines.push(chalk.bold(' ┌─────────────────────────────────────┐'));
|
|
30
|
+
lines.push(chalk.bold(' │ Security Score: ') + chalk.bold[scoreColor](`${result.score}/100 (Grade: ${result.grade})`) + chalk.bold(' │'));
|
|
31
|
+
lines.push(chalk.bold(' └─────────────────────────────────────┘'));
|
|
32
|
+
lines.push('');
|
|
33
|
+
|
|
34
|
+
// Summary
|
|
35
|
+
lines.push(chalk.bold(' FINDINGS SUMMARY'));
|
|
36
|
+
lines.push(chalk.bold(' ─────────────────────────────────────'));
|
|
37
|
+
if (result.summary.critical > 0) {
|
|
38
|
+
lines.push(chalk.red(` ● CRITICAL: ${result.summary.critical}`));
|
|
39
|
+
}
|
|
40
|
+
if (result.summary.high > 0) {
|
|
41
|
+
lines.push(chalk.redBright(` ● HIGH: ${result.summary.high}`));
|
|
42
|
+
}
|
|
43
|
+
if (result.summary.medium > 0) {
|
|
44
|
+
lines.push(chalk.yellow(` ● MEDIUM: ${result.summary.medium}`));
|
|
45
|
+
}
|
|
46
|
+
if (result.summary.low > 0) {
|
|
47
|
+
lines.push(chalk.cyan(` ● LOW: ${result.summary.low}`));
|
|
48
|
+
}
|
|
49
|
+
if (result.summary.info > 0) {
|
|
50
|
+
lines.push(chalk.gray(` ● INFO: ${result.summary.info}`));
|
|
51
|
+
}
|
|
52
|
+
lines.push(chalk.bold(` ─────────────────────────────────────`));
|
|
53
|
+
lines.push(chalk.bold(` TOTAL: ${result.summary.total}`));
|
|
54
|
+
lines.push('');
|
|
55
|
+
|
|
56
|
+
// Detailed findings
|
|
57
|
+
if (result.findings.length > 0) {
|
|
58
|
+
lines.push(chalk.bold(' DETAILED FINDINGS'));
|
|
59
|
+
lines.push(chalk.bold(' ═══════════════════════════════════════════════════════════'));
|
|
60
|
+
lines.push('');
|
|
61
|
+
|
|
62
|
+
// Group by severity
|
|
63
|
+
const severityOrder = [SEVERITY.CRITICAL, SEVERITY.HIGH, SEVERITY.MEDIUM, SEVERITY.LOW, SEVERITY.INFO];
|
|
64
|
+
|
|
65
|
+
for (const severity of severityOrder) {
|
|
66
|
+
const severityFindings = result.findings.filter((f) => f.severity === severity);
|
|
67
|
+
if (severityFindings.length === 0) continue;
|
|
68
|
+
|
|
69
|
+
const color = SEVERITY_COLORS[severity];
|
|
70
|
+
const label = SEVERITY_LABELS[severity];
|
|
71
|
+
|
|
72
|
+
lines.push(chalk[color].bold(` ── ${label} (${severityFindings.length}) ──────────────────────────`));
|
|
73
|
+
lines.push('');
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < severityFindings.length; i++) {
|
|
76
|
+
const finding = severityFindings[i];
|
|
77
|
+
lines.push(chalk[color].bold(` ${i + 1}. [${finding.ruleId}]`));
|
|
78
|
+
lines.push(chalk.white(` ${finding.message}`));
|
|
79
|
+
lines.push(chalk.gray(` File: ${finding.file}${finding.line ? `:${finding.line}` : ''}`));
|
|
80
|
+
|
|
81
|
+
if (finding.code) {
|
|
82
|
+
lines.push(chalk.gray(' ┌──────────────────────────────'));
|
|
83
|
+
const codeLines = finding.code.split('\n');
|
|
84
|
+
for (const codeLine of codeLines) {
|
|
85
|
+
lines.push(chalk.gray(` │ ${codeLine}`));
|
|
86
|
+
}
|
|
87
|
+
lines.push(chalk.gray(' └──────────────────────────────'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (finding.recommendation) {
|
|
91
|
+
lines.push(chalk.green(` ✓ ${finding.recommendation}`));
|
|
92
|
+
}
|
|
93
|
+
lines.push('');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Footer
|
|
99
|
+
lines.push(chalk.bold(' ═══════════════════════════════════════════════════════════'));
|
|
100
|
+
if (result.passed) {
|
|
101
|
+
lines.push(chalk.green.bold(` ✅ PASSED - No findings at or above "${result.threshold}" severity threshold`));
|
|
102
|
+
} else {
|
|
103
|
+
lines.push(chalk.red.bold(` ❌ FAILED - Found issues at or above "${result.threshold}" severity threshold`));
|
|
104
|
+
}
|
|
105
|
+
lines.push(chalk.gray(` Threshold: ${result.threshold} | Exit code: ${result.passed ? 0 : 1}`));
|
|
106
|
+
lines.push('');
|
|
107
|
+
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = consoleReport;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { SEVERITY, SEVERITY_LABELS } = require('../utils/severity');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* HTML Reporter - Beautiful HTML report for stakeholders
|
|
7
|
+
*/
|
|
8
|
+
function htmlReport(result) {
|
|
9
|
+
const severityColors = {
|
|
10
|
+
[SEVERITY.CRITICAL]: '#dc2626',
|
|
11
|
+
[SEVERITY.HIGH]: '#ea580c',
|
|
12
|
+
[SEVERITY.MEDIUM]: '#d97706',
|
|
13
|
+
[SEVERITY.LOW]: '#2563eb',
|
|
14
|
+
[SEVERITY.INFO]: '#6b7280',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const scoreColor = result.score >= 80 ? '#16a34a' : result.score >= 60 ? '#d97706' : '#dc2626';
|
|
18
|
+
|
|
19
|
+
const findingsHtml = result.findings
|
|
20
|
+
.sort((a, b) => {
|
|
21
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
22
|
+
return (order[a.severity] || 5) - (order[b.severity] || 5);
|
|
23
|
+
})
|
|
24
|
+
.map((f) => `
|
|
25
|
+
<div class="finding finding-${f.severity}">
|
|
26
|
+
<div class="finding-header">
|
|
27
|
+
<span class="badge" style="background:${severityColors[f.severity]}">${SEVERITY_LABELS[f.severity]}</span>
|
|
28
|
+
<span class="rule-id">${escapeHtml(f.ruleId)}</span>
|
|
29
|
+
<span class="category">${escapeHtml(f.category)}</span>
|
|
30
|
+
</div>
|
|
31
|
+
<p class="finding-message">${escapeHtml(f.message)}</p>
|
|
32
|
+
<div class="finding-meta">
|
|
33
|
+
<span>📁 ${escapeHtml(f.file)}${f.line ? `:${f.line}` : ''}</span>
|
|
34
|
+
</div>
|
|
35
|
+
${f.code ? `<pre class="code-block"><code>${escapeHtml(f.code)}</code></pre>` : ''}
|
|
36
|
+
${f.recommendation ? `<div class="recommendation">✅ <strong>Recommendation:</strong> ${escapeHtml(f.recommendation)}</div>` : ''}
|
|
37
|
+
</div>
|
|
38
|
+
`)
|
|
39
|
+
.join('\n');
|
|
40
|
+
|
|
41
|
+
return `<!DOCTYPE html>
|
|
42
|
+
<html lang="en">
|
|
43
|
+
<head>
|
|
44
|
+
<meta charset="UTF-8">
|
|
45
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
46
|
+
<title>NGX Security Audit Report - ${escapeHtml(result.projectName || 'Angular Project')}</title>
|
|
47
|
+
<style>
|
|
48
|
+
:root {
|
|
49
|
+
--bg: #0f172a;
|
|
50
|
+
--surface: #1e293b;
|
|
51
|
+
--surface2: #334155;
|
|
52
|
+
--text: #f1f5f9;
|
|
53
|
+
--text-dim: #94a3b8;
|
|
54
|
+
--accent: #38bdf8;
|
|
55
|
+
--success: #22c55e;
|
|
56
|
+
--danger: #ef4444;
|
|
57
|
+
--border: #475569;
|
|
58
|
+
}
|
|
59
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
60
|
+
body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; }
|
|
61
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
|
62
|
+
header { text-align: center; padding: 3rem 0; border-bottom: 1px solid var(--border); margin-bottom: 2rem; }
|
|
63
|
+
header h1 { font-size: 2.5rem; background: linear-gradient(135deg, #38bdf8, #818cf8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 0.5rem; }
|
|
64
|
+
header .subtitle { color: var(--text-dim); font-size: 1.1rem; }
|
|
65
|
+
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
66
|
+
.info-card { background: var(--surface); border-radius: 12px; padding: 1.5rem; border: 1px solid var(--border); }
|
|
67
|
+
.info-card label { color: var(--text-dim); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
68
|
+
.info-card .value { font-size: 1.3rem; font-weight: 600; margin-top: 0.25rem; }
|
|
69
|
+
.score-section { text-align: center; padding: 3rem; background: var(--surface); border-radius: 16px; margin-bottom: 2rem; border: 1px solid var(--border); }
|
|
70
|
+
.score-circle { display: inline-flex; align-items: center; justify-content: center; width: 160px; height: 160px; border-radius: 50%; border: 8px solid ${scoreColor}; margin-bottom: 1rem; }
|
|
71
|
+
.score-number { font-size: 3rem; font-weight: 800; color: ${scoreColor}; }
|
|
72
|
+
.score-grade { font-size: 1.5rem; color: ${scoreColor}; font-weight: 700; }
|
|
73
|
+
.status { display: inline-block; padding: 0.5rem 2rem; border-radius: 50px; font-weight: 700; font-size: 1.1rem; margin-top: 1rem; }
|
|
74
|
+
.status-pass { background: rgba(34, 197, 94, 0.15); color: #22c55e; border: 2px solid #22c55e; }
|
|
75
|
+
.status-fail { background: rgba(239, 68, 68, 0.15); color: #ef4444; border: 2px solid #ef4444; }
|
|
76
|
+
.summary-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1rem; margin-bottom: 2rem; }
|
|
77
|
+
.summary-card { background: var(--surface); border-radius: 12px; padding: 1.2rem; text-align: center; border: 1px solid var(--border); }
|
|
78
|
+
.summary-card .count { font-size: 2rem; font-weight: 800; }
|
|
79
|
+
.summary-card .label { color: var(--text-dim); font-size: 0.85rem; text-transform: uppercase; }
|
|
80
|
+
.findings-section h2 { font-size: 1.5rem; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 2px solid var(--accent); }
|
|
81
|
+
.finding { background: var(--surface); border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; border-left: 4px solid var(--border); border: 1px solid var(--border); }
|
|
82
|
+
.finding-critical { border-left-color: #dc2626; }
|
|
83
|
+
.finding-high { border-left-color: #ea580c; }
|
|
84
|
+
.finding-medium { border-left-color: #d97706; }
|
|
85
|
+
.finding-low { border-left-color: #2563eb; }
|
|
86
|
+
.finding-info { border-left-color: #6b7280; }
|
|
87
|
+
.finding-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
|
|
88
|
+
.badge { padding: 0.2rem 0.75rem; border-radius: 50px; color: white; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; }
|
|
89
|
+
.rule-id { font-family: monospace; color: var(--accent); font-size: 0.9rem; }
|
|
90
|
+
.category { color: var(--text-dim); font-size: 0.85rem; background: var(--surface2); padding: 0.15rem 0.5rem; border-radius: 4px; }
|
|
91
|
+
.finding-message { margin-bottom: 0.75rem; }
|
|
92
|
+
.finding-meta { color: var(--text-dim); font-size: 0.85rem; margin-bottom: 0.75rem; }
|
|
93
|
+
.code-block { background: #0d1117; border-radius: 8px; padding: 1rem; overflow-x: auto; margin: 0.75rem 0; border: 1px solid #30363d; }
|
|
94
|
+
.code-block code { font-family: 'Cascadia Code', 'Fira Code', monospace; font-size: 0.85rem; color: #c9d1d9; white-space: pre; }
|
|
95
|
+
.recommendation { background: rgba(34, 197, 94, 0.08); border: 1px solid rgba(34, 197, 94, 0.2); border-radius: 8px; padding: 0.75rem 1rem; font-size: 0.9rem; color: #86efac; }
|
|
96
|
+
footer { text-align: center; padding: 2rem 0; color: var(--text-dim); border-top: 1px solid var(--border); margin-top: 2rem; font-size: 0.85rem; }
|
|
97
|
+
@media (max-width: 768px) {
|
|
98
|
+
.summary-grid { grid-template-columns: repeat(2, 1fr); }
|
|
99
|
+
.info-grid { grid-template-columns: 1fr; }
|
|
100
|
+
header h1 { font-size: 1.8rem; }
|
|
101
|
+
}
|
|
102
|
+
</style>
|
|
103
|
+
</head>
|
|
104
|
+
<body>
|
|
105
|
+
<div class="container">
|
|
106
|
+
<header>
|
|
107
|
+
<h1>🛡️ NGX Security Audit Report</h1>
|
|
108
|
+
<p class="subtitle">Angular Application Security Analysis powered by ngx-security-audit</p>
|
|
109
|
+
</header>
|
|
110
|
+
|
|
111
|
+
<div class="info-grid">
|
|
112
|
+
<div class="info-card">
|
|
113
|
+
<label>Project</label>
|
|
114
|
+
<div class="value">${escapeHtml(result.projectName || 'Angular Project')}</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="info-card">
|
|
117
|
+
<label>Angular Version</label>
|
|
118
|
+
<div class="value">${escapeHtml(result.angularVersion || 'Unknown')}</div>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="info-card">
|
|
121
|
+
<label>Scan Date</label>
|
|
122
|
+
<div class="value">${new Date(result.scanDate).toLocaleDateString()}</div>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="info-card">
|
|
125
|
+
<label>Files Scanned</label>
|
|
126
|
+
<div class="value">${result.filesScanned}</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="info-card">
|
|
129
|
+
<label>Rules Executed</label>
|
|
130
|
+
<div class="value">${result.rulesExecuted}</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div class="score-section">
|
|
135
|
+
<div class="score-circle">
|
|
136
|
+
<span class="score-number">${result.score}</span>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="score-grade">Grade: ${result.grade}</div>
|
|
139
|
+
<div class="status ${result.passed ? 'status-pass' : 'status-fail'}">
|
|
140
|
+
${result.passed ? '✅ PASSED' : '❌ FAILED'} (threshold: ${escapeHtml(result.threshold)})
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div class="summary-grid">
|
|
145
|
+
<div class="summary-card">
|
|
146
|
+
<div class="count" style="color:#dc2626">${result.summary.critical}</div>
|
|
147
|
+
<div class="label">Critical</div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="summary-card">
|
|
150
|
+
<div class="count" style="color:#ea580c">${result.summary.high}</div>
|
|
151
|
+
<div class="label">High</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="summary-card">
|
|
154
|
+
<div class="count" style="color:#d97706">${result.summary.medium}</div>
|
|
155
|
+
<div class="label">Medium</div>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="summary-card">
|
|
158
|
+
<div class="count" style="color:#2563eb">${result.summary.low}</div>
|
|
159
|
+
<div class="label">Low</div>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="summary-card">
|
|
162
|
+
<div class="count" style="color:#6b7280">${result.summary.info}</div>
|
|
163
|
+
<div class="label">Info</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div class="findings-section">
|
|
168
|
+
<h2>Detailed Findings (${result.summary.total})</h2>
|
|
169
|
+
${result.findings.length === 0 ? '<p style="color: #22c55e; text-align: center; padding: 2rem;">🎉 No security issues found! Your Angular application passed all checks.</p>' : findingsHtml}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<footer>
|
|
173
|
+
<p>Generated by <strong>ngx-security-audit v1.0.0</strong> | ${new Date(result.scanDate).toISOString()}</p>
|
|
174
|
+
<p>OWASP Top 10 Aligned | ${result.rulesExecuted} Security Rules | Angular-Specific Analysis</p>
|
|
175
|
+
</footer>
|
|
176
|
+
</div>
|
|
177
|
+
</body>
|
|
178
|
+
</html>`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function escapeHtml(str) {
|
|
182
|
+
if (!str) return '';
|
|
183
|
+
return String(str)
|
|
184
|
+
.replace(/&/g, '&')
|
|
185
|
+
.replace(/</g, '<')
|
|
186
|
+
.replace(/>/g, '>')
|
|
187
|
+
.replace(/"/g, '"')
|
|
188
|
+
.replace(/'/g, ''');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = htmlReport;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const consoleReport = require('./console-reporter');
|
|
4
|
+
const jsonReport = require('./json-reporter');
|
|
5
|
+
const htmlReport = require('./html-reporter');
|
|
6
|
+
const sarifReport = require('./sarif-reporter');
|
|
7
|
+
|
|
8
|
+
const reporters = {
|
|
9
|
+
console: consoleReport,
|
|
10
|
+
json: jsonReport,
|
|
11
|
+
html: htmlReport,
|
|
12
|
+
sarif: sarifReport,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function getReporter(format) {
|
|
16
|
+
const reporter = reporters[format];
|
|
17
|
+
if (!reporter) {
|
|
18
|
+
throw new Error(`Unknown report format: "${format}". Available formats: ${Object.keys(reporters).join(', ')}`);
|
|
19
|
+
}
|
|
20
|
+
return reporter;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { getReporter, reporters };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { SEVERITY } = require('../utils/severity');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SARIF Reporter - Static Analysis Results Interchange Format
|
|
7
|
+
* Compatible with GitHub Code Scanning, Azure DevOps, and other SARIF tools
|
|
8
|
+
* Spec: https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
|
|
9
|
+
*/
|
|
10
|
+
function sarifReport(result) {
|
|
11
|
+
const severityToLevel = {
|
|
12
|
+
[SEVERITY.CRITICAL]: 'error',
|
|
13
|
+
[SEVERITY.HIGH]: 'error',
|
|
14
|
+
[SEVERITY.MEDIUM]: 'warning',
|
|
15
|
+
[SEVERITY.LOW]: 'note',
|
|
16
|
+
[SEVERITY.INFO]: 'note',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const severityToRank = {
|
|
20
|
+
[SEVERITY.CRITICAL]: 9.5,
|
|
21
|
+
[SEVERITY.HIGH]: 8.0,
|
|
22
|
+
[SEVERITY.MEDIUM]: 5.0,
|
|
23
|
+
[SEVERITY.LOW]: 3.0,
|
|
24
|
+
[SEVERITY.INFO]: 1.0,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Build unique rules list
|
|
28
|
+
const ruleMap = new Map();
|
|
29
|
+
for (const finding of result.findings) {
|
|
30
|
+
if (!ruleMap.has(finding.ruleId)) {
|
|
31
|
+
ruleMap.set(finding.ruleId, {
|
|
32
|
+
id: finding.ruleId,
|
|
33
|
+
name: finding.ruleId.replace(/\//g, '-'),
|
|
34
|
+
shortDescription: { text: finding.message.substring(0, 200) },
|
|
35
|
+
fullDescription: { text: finding.message },
|
|
36
|
+
defaultConfiguration: {
|
|
37
|
+
level: severityToLevel[finding.severity] || 'warning',
|
|
38
|
+
rank: severityToRank[finding.severity] || 5.0,
|
|
39
|
+
},
|
|
40
|
+
properties: {
|
|
41
|
+
tags: ['security', finding.category || 'general'],
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Build results
|
|
48
|
+
const results = result.findings.map((finding) => {
|
|
49
|
+
const sarifResult = {
|
|
50
|
+
ruleId: finding.ruleId,
|
|
51
|
+
level: severityToLevel[finding.severity] || 'warning',
|
|
52
|
+
message: {
|
|
53
|
+
text: finding.message,
|
|
54
|
+
},
|
|
55
|
+
locations: [],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (finding.file && finding.file !== 'project-wide') {
|
|
59
|
+
sarifResult.locations.push({
|
|
60
|
+
physicalLocation: {
|
|
61
|
+
artifactLocation: {
|
|
62
|
+
uri: finding.file.replace(/\\/g, '/'),
|
|
63
|
+
uriBaseId: '%SRCROOT%',
|
|
64
|
+
},
|
|
65
|
+
region: {
|
|
66
|
+
startLine: finding.line || 1,
|
|
67
|
+
startColumn: 1,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (finding.recommendation) {
|
|
74
|
+
sarifResult.fixes = [
|
|
75
|
+
{
|
|
76
|
+
description: { text: finding.recommendation },
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return sarifResult;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const sarif = {
|
|
85
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json',
|
|
86
|
+
version: '2.1.0',
|
|
87
|
+
runs: [
|
|
88
|
+
{
|
|
89
|
+
tool: {
|
|
90
|
+
driver: {
|
|
91
|
+
name: 'ngx-security-audit',
|
|
92
|
+
version: '1.0.0',
|
|
93
|
+
informationUri: 'https://github.com/noredinebahri/ngx-security-audit',
|
|
94
|
+
rules: Array.from(ruleMap.values()),
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
results,
|
|
98
|
+
invocations: [
|
|
99
|
+
{
|
|
100
|
+
executionSuccessful: true,
|
|
101
|
+
endTimeUtc: result.scanDate,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return JSON.stringify(sarif, null, 2);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = sarifReport;
|