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/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, '&amp;')
185
+ .replace(/</g, '&lt;')
186
+ .replace(/>/g, '&gt;')
187
+ .replace(/"/g, '&quot;')
188
+ .replace(/'/g, '&#039;');
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,10 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * JSON Reporter - Machine-readable output for CI/CD pipelines
5
+ */
6
+ function jsonReport(result) {
7
+ return JSON.stringify(result, null, 2);
8
+ }
9
+
10
+ module.exports = jsonReport;
@@ -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;