Skip to content
Merged
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
51 changes: 47 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,36 @@ const STATUS_ICON: Record<string, string> = {
good: `${GRN}✓${R}`, warning: `${YLW}⚠${R}`, missing: `${RED}✗${R}`, error: `${RED}✗${R}`,
};

function getVersion(): string {
try {
const pkg = require('../package.json');
return pkg.version;
} catch {
return '0.0.0';
}
}

function printHelp() {
const v = getVersion();
console.log(`${B}@hailbytes/security-headers${R} v${v}`);
console.log('');
console.log(`${B}Usage:${R}`);
console.log(' security-headers <url> [options]');
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('');
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"');
}

function printReport(r: SecurityHeaderReport) {
const gc = GRADE_COLOR[r.grade] ?? '';
console.log(`\n${B}Security Headers Report${R}`);
Expand All @@ -35,15 +65,28 @@ function printReport(r: SecurityHeaderReport) {

async function main() {
const args = process.argv.slice(2);

if (args.includes('--help') || args.includes('-h')) {
printHelp();
process.exit(0);
}

if (args.includes('--version') || args.includes('-v')) {
console.log(getVersion());
process.exit(0);
}

const jsonMode = args.includes('--json');
const url = args.find(a => !a.startsWith('--'));
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));
if (!url) {
console.error('Usage: security-headers <url> [--json]');
console.error('Example: security-headers https://example.com');
console.error('Usage: security-headers <url> [--json] [--timeout ms] [--help] [--version]');
console.error('Run with --help for full usage information.');
process.exit(1);
}
try {
const report = await analyze(url);
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);
Expand Down
21 changes: 16 additions & 5 deletions src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
export async function fetchHeaders(url: string): Promise<Record<string, string>> {
const res = await fetch(url, { method: 'HEAD', redirect: 'follow' });
const headers: Record<string, string> = {};
res.headers.forEach((value, key) => { headers[key.toLowerCase()] = value; });
return headers;
export interface FetchOptions {
timeoutMs?: number;
}

export async function fetchHeaders(url: string, options?: FetchOptions): Promise<Record<string, string>> {
const timeoutMs = options?.timeoutMs ?? 10000;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, { method: 'HEAD', redirect: 'follow', signal: controller.signal });
const headers: Record<string, string> = {};
res.headers.forEach((value, key) => { headers[key.toLowerCase()] = value; });
return headers;
} finally {
clearTimeout(timer);
}
}
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
export { analyzeHeaders } from './analyzer.js';
export { fetchHeaders } from './fetch.js';
export type { SecurityHeaderReport, HeaderFinding, Grade, HeaderStatus } from './types.js';
export type { FetchOptions } from './fetch.js';

import { fetchHeaders } from './fetch.js';
import { analyzeHeaders } from './analyzer.js';
import type { SecurityHeaderReport } from './types.js';
import type { FetchOptions } from './fetch.js';

export async function analyze(input: string | Record<string, string>): Promise<SecurityHeaderReport> {
export async function analyze(input: string | Record<string, string>, options?: FetchOptions): Promise<SecurityHeaderReport> {
if (typeof input === 'string') {
const headers = await fetchHeaders(input);
const headers = await fetchHeaders(input, options);
return analyzeHeaders(headers, input);
}
return analyzeHeaders(input);
Expand Down
Loading
Loading