@rajadeveloper12/headerguard 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/bin/headerguard.js +26 -0
- package/package.json +22 -0
- package/src/ci.js +54 -0
- package/src/scan.js +122 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import { scanCommand } from '../src/scan.js';
|
|
4
|
+
import { ciCommand } from '../src/ci.js';
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.name('headerguard')
|
|
8
|
+
.description('HTTP Security Header Analyzer & Fixer')
|
|
9
|
+
.version('1.0.0');
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.command('scan <url>')
|
|
13
|
+
.description('Scan a URL for security header issues')
|
|
14
|
+
.option('-s, --stack <stack>', 'Your backend stack: fastapi|express|django|nginx|caddy')
|
|
15
|
+
.option('-j, --json', 'Output raw JSON')
|
|
16
|
+
.option('--fail-below <score>', 'Exit with code 1 if score below threshold (for CI)')
|
|
17
|
+
.action(scanCommand);
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.command('ci <url>')
|
|
21
|
+
.description('CI mode: exit 1 if score below --fail-below threshold')
|
|
22
|
+
.option('--fail-below <score>', 'Minimum passing score', '70')
|
|
23
|
+
.option('-s, --stack <stack>', 'Backend stack')
|
|
24
|
+
.action(ciCommand);
|
|
25
|
+
|
|
26
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rajadeveloper12/headerguard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "HTTP Security Header Analyzer & Fixer CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"headerguard": "./bin/headerguard.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"No tests yet\""
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["security", "headers", "cors", "csp", "hsts", "devtools"],
|
|
14
|
+
"author": "ClearFix.co",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"chalk": "^5.3.0",
|
|
18
|
+
"node-fetch": "^3.3.2",
|
|
19
|
+
"ora": "^8.0.1",
|
|
20
|
+
"commander": "^12.1.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/ci.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fetch from 'node-fetch';
|
|
3
|
+
|
|
4
|
+
const API_URL = process.env.HEADERGUARD_API || 'https://api.headerguard.dev';
|
|
5
|
+
|
|
6
|
+
export async function ciCommand(url, options) {
|
|
7
|
+
if (!url.startsWith('http')) url = 'https://' + url;
|
|
8
|
+
const threshold = parseInt(options.failBelow || '70');
|
|
9
|
+
|
|
10
|
+
console.log(`::group::HeaderGuard Security Scan`);
|
|
11
|
+
console.log(`Scanning: ${url}`);
|
|
12
|
+
console.log(`Threshold: ${threshold}`);
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(`${API_URL}/scan`, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
body: JSON.stringify({ url, stack: options.stack || null }),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
|
|
23
|
+
console.log(`Score: ${data.score}/100 (Grade: ${data.grade})`);
|
|
24
|
+
console.log(`Summary: ${data.summary}`);
|
|
25
|
+
console.log('');
|
|
26
|
+
|
|
27
|
+
// GitHub Actions annotations
|
|
28
|
+
for (const [key, result] of Object.entries(data.headers)) {
|
|
29
|
+
if (result.status === 'missing') {
|
|
30
|
+
console.log(`::error::Missing security header: ${key.toUpperCase()} - ${result.issue}`);
|
|
31
|
+
} else if (result.status === 'warn') {
|
|
32
|
+
console.log(`::warning::Security header warning: ${key.toUpperCase()} - ${result.issue}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`::endgroup::`);
|
|
37
|
+
|
|
38
|
+
// Set output for GitHub Actions
|
|
39
|
+
console.log(`::set-output name=score::${data.score}`);
|
|
40
|
+
console.log(`::set-output name=grade::${data.grade}`);
|
|
41
|
+
|
|
42
|
+
if (data.score < threshold) {
|
|
43
|
+
console.log(`::error::HeaderGuard: Score ${data.score} below threshold ${threshold}. Deploy blocked.`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(`HeaderGuard: PASSED (${data.score}/${threshold} threshold)`);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.log(`::error::HeaderGuard scan failed: ${err.message}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/scan.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
|
|
5
|
+
const API_URL = process.env.HEADERGUARD_API || 'https://headerguard-api-production.up.railway.app';
|
|
6
|
+
|
|
7
|
+
const HEADER_LABELS = {
|
|
8
|
+
cors: 'CORS (Access-Control-Allow-Origin)',
|
|
9
|
+
csp: 'Content-Security-Policy',
|
|
10
|
+
hsts: 'Strict-Transport-Security',
|
|
11
|
+
x_frame_options: 'X-Frame-Options',
|
|
12
|
+
x_content_type: 'X-Content-Type-Options',
|
|
13
|
+
referrer_policy: 'Referrer-Policy',
|
|
14
|
+
permissions_policy: 'Permissions-Policy',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function gradeColor(grade) {
|
|
18
|
+
if (grade.startsWith('A')) return chalk.greenBright(grade);
|
|
19
|
+
if (grade === 'B') return chalk.green(grade);
|
|
20
|
+
if (grade === 'C') return chalk.yellow(grade);
|
|
21
|
+
if (grade === 'D') return chalk.red(grade);
|
|
22
|
+
return chalk.bgRed.white(grade);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function statusIcon(status) {
|
|
26
|
+
if (status === 'pass') return chalk.green('✓');
|
|
27
|
+
if (status === 'warn') return chalk.yellow('⚠');
|
|
28
|
+
if (status === 'missing') return chalk.red('✗');
|
|
29
|
+
return chalk.red('✗');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function scoreBar(score, max) {
|
|
33
|
+
const pct = Math.round((score / max) * 10);
|
|
34
|
+
const filled = '█'.repeat(pct);
|
|
35
|
+
const empty = '░'.repeat(10 - pct);
|
|
36
|
+
const color = pct >= 8 ? chalk.green : pct >= 5 ? chalk.yellow : chalk.red;
|
|
37
|
+
return color(filled + empty) + chalk.dim(` ${score}/${max}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function scanCommand(url, options) {
|
|
41
|
+
if (!url.startsWith('http')) url = 'https://' + url;
|
|
42
|
+
|
|
43
|
+
const spinner = ora(`Scanning ${chalk.cyan(url)}...`).start();
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(`${API_URL}/scan`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({ url, stack: options.stack || null }),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
const err = await res.json();
|
|
54
|
+
spinner.fail(chalk.red(`Scan failed: ${err.detail || res.statusText}`));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const data = await res.json();
|
|
59
|
+
spinner.stop();
|
|
60
|
+
|
|
61
|
+
if (options.json) {
|
|
62
|
+
console.log(JSON.stringify(data, null, 2));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Header
|
|
67
|
+
console.log('');
|
|
68
|
+
console.log(chalk.bold(' HeaderGuard Security Report'));
|
|
69
|
+
console.log(chalk.dim(' ' + '─'.repeat(50)));
|
|
70
|
+
console.log(` ${chalk.dim('URL')} ${chalk.cyan(data.url)}`);
|
|
71
|
+
console.log(` ${chalk.dim('Score')} ${scoreBar(data.score, 100)}`);
|
|
72
|
+
console.log(` ${chalk.dim('Grade')} ${gradeColor(data.grade)}`);
|
|
73
|
+
console.log(` ${chalk.dim('Summary')} ${data.summary}`);
|
|
74
|
+
console.log('');
|
|
75
|
+
|
|
76
|
+
// Headers breakdown
|
|
77
|
+
console.log(chalk.bold(' Header Analysis'));
|
|
78
|
+
console.log(chalk.dim(' ' + '─'.repeat(50)));
|
|
79
|
+
|
|
80
|
+
for (const [key, result] of Object.entries(data.headers)) {
|
|
81
|
+
const label = HEADER_LABELS[key] || key;
|
|
82
|
+
const icon = statusIcon(result.status);
|
|
83
|
+
const bar = scoreBar(result.score, result.max);
|
|
84
|
+
|
|
85
|
+
console.log(` ${icon} ${chalk.white(label.padEnd(35))} ${bar}`);
|
|
86
|
+
if (result.issue) {
|
|
87
|
+
console.log(` ${chalk.dim(result.issue)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fixes
|
|
92
|
+
const fixKeys = Object.keys(data.fixes);
|
|
93
|
+
if (fixKeys.length > 0) {
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log(chalk.bold(' Recommended Fixes'));
|
|
96
|
+
console.log(chalk.dim(' ' + '─'.repeat(50)));
|
|
97
|
+
|
|
98
|
+
for (const key of fixKeys) {
|
|
99
|
+
const fix = data.fixes[key];
|
|
100
|
+
const label = HEADER_LABELS[key] || key;
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(` ${chalk.yellow('→')} ${chalk.bold(label)} ${chalk.dim(`(${fix.stack})`)}`);
|
|
103
|
+
const lines = fix.snippet.split('\n');
|
|
104
|
+
lines.forEach(line => console.log(` ${chalk.dim(line)}`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log(chalk.dim(' Docs: https://headerguard.dev/docs'));
|
|
110
|
+
console.log('');
|
|
111
|
+
|
|
112
|
+
// CI fail-below
|
|
113
|
+
if (options.failBelow && data.score < parseInt(options.failBelow)) {
|
|
114
|
+
console.log(chalk.red(` ✗ Score ${data.score} is below threshold ${options.failBelow}. Failing.`));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
} catch (err) {
|
|
119
|
+
spinner.fail(chalk.red(`Error: ${err.message}`));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|