mirror of
https://github.com/ivuorinen/gh-codeql-report.git
synced 2026-02-02 08:44:06 +00:00
Initial commit
This commit is contained in:
10
src/formatters/json.ts
Normal file
10
src/formatters/json.ts
Normal 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
157
src/formatters/markdown.ts
Normal 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
78
src/formatters/sarif.ts
Normal 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
71
src/formatters/text.ts
Normal 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');
|
||||
}
|
||||
Reference in New Issue
Block a user