Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ npx @hailbytes/security-headers https://example.com
# Output raw JSON
npx @hailbytes/security-headers https://example.com --json

# Use as a CI gate (exits 1 on grade D or F)
# Use as a CI gate (exits 1 on grade D or F — default threshold)
npx @hailbytes/security-headers https://staging.example.com || echo "Security headers gate failed"

# Stricter CI gate: fail on C or below
npx @hailbytes/security-headers https://staging.example.com --fail-on C
```

### Library — analyze a URL
Expand Down
39 changes: 31 additions & 8 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ const RED = '\x1b[31m';
const GRN = '\x1b[32m';
const YLW = '\x1b[33m';

const GRADES = ['A+', 'A', 'B', 'C', 'D', 'F'] as const;
type Grade = typeof GRADES[number];

// Returns true when reportGrade is at or worse than threshold (higher index = worse).
function gradeAtOrBelow(reportGrade: string, threshold: Grade): boolean {
return GRADES.indexOf(reportGrade as Grade) >= GRADES.indexOf(threshold);
}

const GRADE_COLOR: Record<string, string> = {
'A+': '\x1b[92m', A: GRN, B: YLW, C: YLW, D: RED, F: '\x1b[91m',
};
Expand All @@ -37,16 +45,19 @@ function printHelp() {
console.log(' npx @hailbytes/security-headers <url> [options]');
console.log('');
console.log(`${B}Options:${R}`);
console.log(' --json Output report as JSON');
console.log(' --timeout ms Fetch timeout in milliseconds (default: 10000)');
console.log(' --version Print version and exit');
console.log(' --help Print this help and exit');
console.log(' --json Output report as JSON');
console.log(' --timeout ms Fetch timeout in milliseconds (default: 10000)');
console.log(' --fail-on grade Exit 1 when grade is at or below this threshold');
console.log(' (default: D — exits 1 on D or F)');
console.log(' Valid grades: A+, A, B, C, D, F');
console.log(' --version Print version and exit');
console.log(' --help Print this help and exit');
console.log('');
console.log(`${B}Examples:${R}`);
console.log(' security-headers https://example.com');
console.log(' security-headers https://example.com --json');
console.log(' security-headers https://example.com --timeout 5000');
console.log(' security-headers https://staging.example.com || echo "Gate failed"');
console.log(' security-headers https://staging.example.com --fail-on C || echo "Gate failed"');
}

function printReport(r: SecurityHeaderReport) {
Expand Down Expand Up @@ -82,17 +93,29 @@ async function main() {
const jsonMode = args.includes('--json');
const timeoutArg = args.find((a, i) => a === '--timeout' && args[i + 1]);
const timeoutMs = timeoutArg ? parseInt(args[args.indexOf('--timeout') + 1], 10) : undefined;
const url = args.find(a => !a.startsWith('--') && a !== String(timeoutMs));

const failOnIdx = args.indexOf('--fail-on');
const failOnRaw = failOnIdx !== -1 ? args[failOnIdx + 1] : undefined;
let failOn: Grade = 'D';
if (failOnRaw !== undefined) {
if (!(GRADES as readonly string[]).includes(failOnRaw)) {
console.error(`Invalid --fail-on value: "${failOnRaw}". Valid grades: ${GRADES.join(', ')}`);
process.exit(1);
}
failOn = failOnRaw as Grade;
}

const url = args.find(a => !a.startsWith('--') && a !== String(timeoutMs) && a !== failOnRaw);
if (!url) {
console.error('Usage: security-headers <url> [--json] [--timeout ms] [--help] [--version]');
console.error('Usage: security-headers <url> [--json] [--timeout ms] [--fail-on grade] [--help] [--version]');
console.error('Run with --help for full usage information.');
process.exit(1);
}
try {
const report = await analyze(url, timeoutMs !== undefined ? { timeoutMs } : undefined);
if (jsonMode) { console.log(JSON.stringify(report, null, 2)); }
else { printReport(report); }
if (report.grade === 'D' || report.grade === 'F') process.exit(1);
if (gradeAtOrBelow(report.grade, failOn)) process.exit(1);
} catch (err) {
console.error(`Error: ${(err as Error).message}`);
process.exit(1);
Expand Down
Loading