Initial commit

This commit is contained in:
2025-09-30 22:34:56 +03:00
commit fafd5e89d4
28 changed files with 6481 additions and 0 deletions

10
src/formatters/json.ts Normal file
View File

@@ -0,0 +1,10 @@
import type { CodeQLAlert } from '../lib/codeql.js';
import { type DetailLevel, filterAlertByDetail } from '../lib/types.js';
/**
* Format alerts as JSON
*/
export function formatAsJSON(alerts: CodeQLAlert[], detailLevel: DetailLevel = 'medium'): string {
const filteredAlerts = alerts.map((alert) => filterAlertByDetail(alert, detailLevel));
return JSON.stringify(filteredAlerts, null, 2);
}

157
src/formatters/markdown.ts Normal file
View File

@@ -0,0 +1,157 @@
import type { CodeQLAlert } from '../lib/codeql.js';
import {
type DetailLevel,
type FullAlert,
filterAlertByDetail,
type MediumAlert,
type MinimumAlert,
} from '../lib/types.js';
/**
* Generate a markdown table from 2D array data
* Exported for testing edge cases
*/
export function generateMarkdownTable(data: string[][]): string {
if (data.length === 0) return '';
const [headers, ...rows] = data;
// Calculate column widths
const columnWidths = headers.map((header, i) => {
const maxWidth = Math.max(header.length, ...rows.map((row) => row[i]?.length || 0));
return maxWidth;
});
// Build table
const lines: string[] = [];
// Header row
const headerRow = headers.map((header, i) => header.padEnd(columnWidths[i])).join(' | ');
lines.push(`| ${headerRow} |`);
// Separator row
const separator = columnWidths.map((width) => '-'.repeat(width)).join(' | ');
lines.push(`| ${separator} |`);
// Data rows
for (const row of rows) {
// Pad row with empty strings if it's shorter than headers
const paddedRow = Array.from({ length: headers.length }, (_, i) => row[i] || '');
const dataRow = paddedRow.map((cell, i) => cell.padEnd(columnWidths[i])).join(' | ');
lines.push(`| ${dataRow} |`);
}
return lines.join('\n');
}
/**
* Format alerts as Markdown
*/
export function formatAsMarkdown(
alerts: CodeQLAlert[],
repoName: string,
detailLevel: DetailLevel = 'medium',
): string {
const lines: string[] = [];
lines.push(`# CodeQL Security Scan Report`);
lines.push('');
lines.push(`**Repository:** ${repoName}`);
lines.push(`**Total Alerts:** ${alerts.length}`);
lines.push(`**Detail Level:** ${detailLevel}`);
lines.push(`**Generated:** ${new Date().toISOString()}`);
lines.push('');
lines.push('---');
lines.push('');
// Summary table
lines.push('## Summary by Severity');
lines.push('');
const severityCounts = alerts.reduce(
(acc, alert) => {
const severity = alert.rule.severity.toLowerCase();
acc[severity] = (acc[severity] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
const summaryTableData = [
['Severity', 'Count'],
...Object.entries(severityCounts).map(([severity, count]) => [severity, count.toString()]),
];
lines.push(generateMarkdownTable(summaryTableData));
lines.push('');
// Detailed alerts
lines.push('## Detailed Alerts');
lines.push('');
for (const alert of alerts) {
const filtered = filterAlertByDetail(alert, detailLevel);
// Handle raw format - return as code block
if (detailLevel === 'raw') {
lines.push('```json');
lines.push(JSON.stringify(filtered, null, 2));
lines.push('```');
lines.push('');
lines.push('---');
lines.push('');
continue;
}
// Type assertion: after raw check, we know filtered is a flattened alert type
const flatAlert = filtered as MinimumAlert | MediumAlert | FullAlert;
lines.push(`### Alert #${flatAlert.number}: ${flatAlert.rule_name}`);
lines.push('');
lines.push(`**Rule ID:** \`${flatAlert.rule_id}\``);
lines.push(`**Severity:** ${flatAlert.severity}`);
// Description only in medium and full
if ('rule_description' in flatAlert) {
lines.push(`**Description:** ${flatAlert.rule_description}`);
}
lines.push('');
lines.push('#### Location');
lines.push('');
lines.push(`- **File:** \`${flatAlert.file_path}\``);
lines.push(`- **Lines:** ${flatAlert.start_line}-${flatAlert.end_line}`);
// Columns only in medium and full
if ('start_column' in flatAlert) {
lines.push(`- **Columns:** ${flatAlert.start_column}-${flatAlert.end_column}`);
}
lines.push('');
lines.push('#### Message');
lines.push('');
lines.push(flatAlert.message);
// Details section - commit is now in all levels
lines.push('');
lines.push('#### Details');
lines.push('');
lines.push(`- **Commit:** \`${flatAlert.commit_sha}\``);
// State only in medium and full
if ('state' in flatAlert) {
lines.push(`- **State:** ${flatAlert.state}`);
}
// Reference only in full
if ('ref' in flatAlert) {
lines.push(`- **Reference:** ${flatAlert.ref}`);
}
lines.push('');
lines.push('---');
lines.push('');
}
return lines.join('\n');
}

78
src/formatters/sarif.ts Normal file
View File

@@ -0,0 +1,78 @@
import { SarifBuilder, SarifResultBuilder, SarifRunBuilder } from 'node-sarif-builder';
import type { CodeQLAlert } from '../lib/codeql.js';
import {
type DetailLevel,
type FullAlert,
filterAlertByDetail,
type MediumAlert,
type MinimumAlert,
} from '../lib/types.js';
/**
* Format alerts as SARIF (Static Analysis Results Interchange Format)
*/
export function formatAsSARIF(
alerts: CodeQLAlert[],
_repoName: string,
detailLevel: DetailLevel = 'medium',
): string {
// For raw format, return alerts as JSON (SARIF doesn't make sense for raw)
if (detailLevel === 'raw') {
return JSON.stringify(alerts, null, 2);
}
const sarifBuilder = new SarifBuilder();
// Tool version only available in full mode
let toolVersion = '1.0.0';
if (detailLevel === 'full' && alerts.length > 0) {
const fullAlert = filterAlertByDetail(alerts[0], 'full');
if ('tool_version' in fullAlert) {
toolVersion = fullAlert.tool_version;
}
}
const runBuilder = new SarifRunBuilder().initSimple({
toolDriverName: 'CodeQL',
toolDriverVersion: toolVersion,
});
for (const alert of alerts) {
const filtered = filterAlertByDetail(alert, detailLevel);
// Type assertion: we know filtered is a flattened alert type (not raw, checked above)
const flatAlert = filtered as MinimumAlert | MediumAlert | FullAlert;
const result = new SarifResultBuilder();
// SARIF requires certain minimum fields
// For minimum level, we use line numbers but set column to 1 if not available
const startColumn = 'start_column' in flatAlert ? flatAlert.start_column : 1;
result.initSimple({
ruleId: flatAlert.rule_id,
level: mapSeverityToLevel(flatAlert.severity),
messageText: flatAlert.message,
fileUri: flatAlert.file_path,
startLine: flatAlert.start_line,
startColumn,
});
runBuilder.addResult(result);
}
sarifBuilder.addRun(runBuilder);
// buildSarifJsonString returns a JSON string
return sarifBuilder.buildSarifJsonString();
}
function mapSeverityToLevel(severity: string): 'error' | 'warning' | 'note' {
switch (severity.toLowerCase()) {
case 'error':
case 'critical':
return 'error';
case 'warning':
case 'medium':
return 'warning';
default:
return 'note';
}
}

71
src/formatters/text.ts Normal file
View File

@@ -0,0 +1,71 @@
import type { CodeQLAlert } from '../lib/codeql.js';
import {
type DetailLevel,
type FullAlert,
filterAlertByDetail,
type MediumAlert,
type MinimumAlert,
} from '../lib/types.js';
/**
* Format alerts as plain text
*/
export function formatAsText(alerts: CodeQLAlert[], detailLevel: DetailLevel = 'medium'): string {
const lines: string[] = [];
lines.push(`CodeQL Security Scan Report`);
lines.push(`Total Alerts: ${alerts.length}`);
lines.push(`Detail Level: ${detailLevel}`);
lines.push(`${'='.repeat(80)}\n`);
for (const alert of alerts) {
const filtered = filterAlertByDetail(alert, detailLevel);
// Handle raw format - return original JSON-like structure
if (detailLevel === 'raw') {
lines.push(JSON.stringify(filtered, null, 2));
lines.push(`${'-'.repeat(80)}\n`);
continue;
}
// Type assertion: after raw check, we know filtered is a flattened alert type
const flatAlert = filtered as MinimumAlert | MediumAlert | FullAlert;
lines.push(`Alert #${flatAlert.number}`);
lines.push(`Rule: ${flatAlert.rule_id}`);
lines.push(`Name: ${flatAlert.rule_name}`);
lines.push(`Severity: ${flatAlert.severity}`);
// Description only in medium and full
if ('rule_description' in flatAlert) {
lines.push(`Description: ${flatAlert.rule_description}`);
}
lines.push('');
lines.push('Location:');
lines.push(` File: ${flatAlert.file_path}`);
lines.push(` Lines: ${flatAlert.start_line}-${flatAlert.end_line}`);
// Columns only in medium and full
if ('start_column' in flatAlert) {
lines.push(` Columns: ${flatAlert.start_column}-${flatAlert.end_column}`);
}
lines.push('');
lines.push('Message:');
lines.push(` ${flatAlert.message}`);
// Commit is now in all levels
lines.push('');
lines.push(`Commit: ${flatAlert.commit_sha}`);
// State only in medium and full
if ('state' in flatAlert) {
lines.push(`State: ${flatAlert.state}`);
}
lines.push(`${'-'.repeat(80)}\n`);
}
return lines.join('\n');
}