From a2d249090f517fcf2b858b486d2d50924d79ea0c Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Mon, 5 Jan 2026 15:20:42 -0600 Subject: [PATCH 01/27] feat: add script to generate comprehensive code scanning coverage report --- gh-cli/README.md | 14 + gh-cli/get-code-scanning-coverage-report.sh | 702 ++++++++++++++++++++ 2 files changed, 716 insertions(+) create mode 100755 gh-cli/get-code-scanning-coverage-report.sh diff --git a/gh-cli/README.md b/gh-cli/README.md index d4e0bb1..d3974d6 100644 --- a/gh-cli/README.md +++ b/gh-cli/README.md @@ -762,6 +762,20 @@ Gets the branch protection status check contexts. See the [docs](https://docs.github.com/en/rest/branches/branch-protection?apiVersion=2022-11-28#get-all-status-check-contexts) for more information. +### get-code-scanning-coverage-report.sh + +Generates a comprehensive code scanning coverage report for all repositories in an organization. The report includes the default branch, last repository update time, detected languages, CodeQL enablement status, latest scan date, scanned languages, unscanned CodeQL-supported languages, open alerts count, and analysis errors/warnings. This helps identify coverage gaps (e.g., a scan done 2 years ago indicates the team is not actively using Code Scanning). + +See the script header comments for usage details. + +When using `--output`, the script generates actionable sub-reports: + +- `*-disabled.csv` - Repos with CodeQL disabled or no scans +- `*-stale.csv` - Repos modified >90 days after last scan +- `*-missing-languages.csv` - Repos scanning but missing some CodeQL languages +- `*-open-alerts.csv` - Repos with open code scanning alerts +- `*-analysis-issues.csv` - Repos with analysis errors or warnings + ### get-code-scanning-status-for-every-repository.sh Get code scanning analyses status for every repository in an organization. diff --git a/gh-cli/get-code-scanning-coverage-report.sh b/gh-cli/get-code-scanning-coverage-report.sh new file mode 100755 index 0000000..01d9c90 --- /dev/null +++ b/gh-cli/get-code-scanning-coverage-report.sh @@ -0,0 +1,702 @@ +#!/bin/bash + +# v1.0.0 + +# This script generates a comprehensive code scanning coverage report for all repositories +# in an organization. It provides insight into which repositories are actively using Code +# Scanning by showing the last scan date, which helps identify coverage gaps (e.g., a scan +# done 2 years ago indicates the team is not actively using Code Scanning). + +# Inputs: +# 1. ORG_NAME: The name of the organization (first positional argument) +# 2. --hostname (optional): GitHub Enterprise Server hostname +# 3. --debug (optional): Enable debug output +# 4. --output (optional): Write CSV output to a file instead of stdout +# 5. --check-workflows (optional): Check GitHub Actions workflow status for CodeQL +# 6. --check-actions (optional): Check if .github/workflows exists and report unscanned actions +# 7. --sample (optional): Sample 25 random repositories instead of processing all +# 8. --repo (optional): Check a single repository instead of all repos in the organization + +# How to call: +# ./get-code-scanning-coverage-report.sh +# ./get-code-scanning-coverage-report.sh --output report.csv +# ./get-code-scanning-coverage-report.sh --hostname github.example.com --output report.csv +# ./get-code-scanning-coverage-report.sh --check-workflows --output report.csv +# ./get-code-scanning-coverage-report.sh --check-actions --output report.csv +# ./get-code-scanning-coverage-report.sh --sample --output report.csv +# ./get-code-scanning-coverage-report.sh --repo my-repo --output report.csv + +# Output format: CSV with the following columns: +# - Repository: Repository name +# - Default Branch: The default branch of the repository +# - Last Updated: When the repository was last updated +# - Languages: Languages detected in the repository +# - CodeQL Enabled: Whether code scanning is enabled and has results: +# - Yes: Code scanning analyses exist (scans have been uploaded) +# - No Scans: Code scanning is enabled but no analyses uploaded yet +# - Disabled: Code Security is disabled on the repository +# - Requires GHAS: GitHub Advanced Security must be enabled first +# - No: Code scanning feature is not accessible (404) +# - Last Default Branch Scan Date: Date of the most recent code scanning analysis on the default branch +# - Scanned Languages: Languages that have been scanned by CodeQL +# - Unscanned CodeQL Languages: CodeQL-supported languages in the repo that are NOT being scanned +# - Open Alerts: Number of open code scanning alerts (security vulnerabilities found) +# - Analysis Errors: Any errors reported in the most recent CodeQL analysis result +# - Analysis Warnings: Any warnings reported in the most recent CodeQL analysis result +# - Workflow Status: (optional, with --check-workflows) Status of the most recent CodeQL workflow run + +# Important Notes: +# - Requires 'gh' CLI to be installed and authenticated +# - CodeQL supported languages: C/C++, C#, Go, Java/Kotlin, JavaScript/TypeScript, Python, Ruby, Swift + +# Tested runtime: +# - bash: 3.2.57(1)-release (arm64-apple-darwin25) +# - gh: gh version 2.83.2 (2025-12-10) +# - awk: 20200816 +# - sed: BSD sed (macOS) + +DEBUG_MODE=0 +OUTPUT_FILE="" +ORG_NAME="" +REPO_NAME="" +HOSTNAME="" +CHECK_WORKFLOWS=0 +CHECK_ACTIONS=0 +SAMPLE_MODE=0 +SAMPLE_SIZE=25 + +# CodeQL supported languages (normalized to lowercase for comparison) +# See: https://codeql.github.com/docs/codeql-overview/supported-languages-and-frameworks/ +CODEQL_SUPPORTED_LANGUAGES="c c++ cpp csharp go java kotlin javascript typescript python ruby swift" + +# Function to handle debug messages +debug() { + if [ "$DEBUG_MODE" -eq 1 ]; then + echo "DEBUG: $*" >&2 + fi +} + +# Function to handle errors +error() { + echo "ERROR: $*" >&2 + exit 1 +} + +# Parse arguments +while [ "$#" -gt 0 ]; do + case "$1" in + --debug) + DEBUG_MODE=1 + shift + ;; + --hostname) + HOSTNAME="$2" + shift 2 + ;; + --output) + OUTPUT_FILE="$2" + shift 2 + ;; + --check-workflows) + CHECK_WORKFLOWS=1 + shift + ;; + --check-actions) + CHECK_ACTIONS=1 + shift + ;; + --sample) + SAMPLE_MODE=1 + shift + ;; + --repo) + REPO_NAME="$2" + shift 2 + ;; + -*) + error "Unknown option: $1" + ;; + *) + ORG_NAME="$1" + shift + ;; + esac +done + +# Validate inputs +if [ -z "$ORG_NAME" ]; then + echo "Usage: $0 [--debug] [--hostname ] [--output ] [--check-workflows] [--check-actions] [--sample] [--repo ] " + echo "" + echo "Options:" + echo " --repo Check a single repository instead of all repos in the organization" + echo " --sample Sample 25 random repositories" + echo " --check-workflows Include CodeQL workflow status" + echo " --check-actions Check for unscanned GitHub Actions workflows" + exit 1 +fi + +# Check for required tools +command -v gh >/dev/null 2>&1 || error "gh CLI is required but not installed" + +# Build hostname flag for gh CLI +GH_HOST_FLAG="" +if [ -n "$HOSTNAME" ]; then + GH_HOST_FLAG="--hostname $HOSTNAME" +fi + +# Function to convert string to lowercase (portable) +to_lowercase() { + echo "$1" | tr '[:upper:]' '[:lower:]' +} + +# Function to check if a language is CodeQL scannable +is_codeql_language() { + local lang + lang=$(to_lowercase "$1") + # Handle C# separately since it has a special character + if [ "$lang" = "c#" ]; then + return 0 + fi + # Check if language is in the supported list + echo " $CODEQL_SUPPORTED_LANGUAGES " | grep -q " $lang " +} + +# Function to get all code scanning analyses for a repo (to extract scanned languages) +get_scanning_info() { + local repo="$1" + local default_branch="$2" + debug "Fetching code scanning analyses for $repo (default branch: $default_branch)" + + local response + # shellcheck disable=SC2086 + response=$(gh api $GH_HOST_FLAG "/repos/$ORG_NAME/$repo/code-scanning/analyses?per_page=100&ref=refs/heads/$default_branch" 2>&1) + + # Check for errors + if echo "$response" | grep -qi "no analysis found\|not found\|Advanced Security\|HTTP 404\|HTTP 403"; then + echo "NO_RESULTS||||||" + return + fi + + # Check if response is an array with data + local count + count=$(echo "$response" | jq -r 'if type == "array" then length else 0 end' 2>/dev/null) + + if [ "$count" = "0" ] || [ -z "$count" ]; then + echo "NO_RESULTS||||||" + return + fi + + # Get the most recent analysis date + local last_scan_date + last_scan_date=$(echo "$response" | jq -r '.[0].created_at // empty' 2>/dev/null) + + # Get the tool name from most recent scan + local tool_name + tool_name=$(echo "$response" | jq -r '.[0].tool.name // empty' 2>/dev/null) + + # Get error from the most recent analysis (if any) + local analysis_error + analysis_error=$(echo "$response" | jq -r '.[0].error // empty' 2>/dev/null) + + # Get warning from the most recent analysis (if any) + local analysis_warning + analysis_warning=$(echo "$response" | jq -r '.[0].warning // empty' 2>/dev/null) + + # Extract all unique scanned languages from the analyses + # The category field often contains language info like "language:python" or "/language:javascript-typescript" + local scanned_languages + scanned_languages=$(echo "$response" | jq -r '[.[].category // empty] | unique | .[]' 2>/dev/null | \ + grep -oE 'language:[a-zA-Z0-9_-]+' | \ + sed 's/language://g' | \ + sort -u | \ + tr '\n' ';' | \ + sed 's/;$//') + + # If no languages found in category, try to infer from tool name + if [ -z "$scanned_languages" ]; then + scanned_languages=$(echo "$response" | jq -r '[.[].tool.name // empty] | unique | join(";")' 2>/dev/null) + fi + + echo "${last_scan_date}|${tool_name}|${scanned_languages}|${analysis_error}|${analysis_warning}" +} + +# Function to get repository languages +get_repo_languages() { + local repo="$1" + debug "Fetching languages for $repo" + + local response + # shellcheck disable=SC2086 + response=$(gh api $GH_HOST_FLAG "/repos/$ORG_NAME/$repo/languages" 2>/dev/null) + + if [ -z "$response" ] || [ "$response" = "{}" ]; then + echo "" + return + fi + + echo "$response" | jq -r 'keys | join(";")' 2>/dev/null +} + +# Function to check CodeQL/code scanning enablement status +check_codeql_status() { + local repo="$1" + debug "Checking code scanning status for $repo" + + # Try to access code scanning analyses endpoint + local response + # shellcheck disable=SC2086 + response=$(gh api $GH_HOST_FLAG "/repos/$ORG_NAME/$repo/code-scanning/analyses?per_page=1" 2>&1) + + # Check various response scenarios + if echo "$response" | grep -qi "Advanced Security must be enabled"; then + echo "Requires GHAS" + return + fi + + # Code Security disabled (newer GitHub terminology) + if echo "$response" | grep -qi "Code Security must be enabled"; then + echo "Disabled" + return + fi + + # "no analysis found" means code scanning IS enabled, just no scans uploaded yet + if echo "$response" | grep -qi "no analysis found"; then + echo "No Scans" + return + fi + + # 404 typically means code scanning feature is not accessible + if echo "$response" | grep -qi "HTTP 404\|not found"; then + echo "No" + return + fi + + # If we got an array response (even empty), code scanning is accessible + if echo "$response" | jq -e 'type == "array"' 2>/dev/null | grep -q true; then + local count + count=$(echo "$response" | jq 'length' 2>/dev/null) + if [ "$count" -gt 0 ]; then + echo "Yes" + else + # Empty array means enabled but no scans yet + echo "No Scans" + fi + return + fi + + echo "Unknown" +} + +# Function to get open code scanning alerts count +get_open_alerts_count() { + local repo="$1" + debug "Fetching open alerts for $repo" + + local response + # shellcheck disable=SC2086 + response=$(gh api $GH_HOST_FLAG "/repos/$ORG_NAME/$repo/code-scanning/alerts?state=open&per_page=1" 2>&1) + + # Check for errors + if echo "$response" | grep -qi "no analysis found\|not found\|Advanced Security\|HTTP 404\|HTTP 403"; then + echo "N/A" + return + fi + + # Get total count using pagination to handle repos with >100 alerts + # shellcheck disable=SC2086 + local count_response + count_response=$(gh api $GH_HOST_FLAG --paginate "/repos/$ORG_NAME/$repo/code-scanning/alerts?state=open&per_page=100" --jq 'length' 2>/dev/null | awk '{sum += $1} END {print sum}') + + if [ -z "$count_response" ] || [ "$count_response" = "0" ]; then + echo "0" + else + echo "$count_response" + fi +} + +# Function to check the most recent CodeQL workflow run status +get_codeql_workflow_status() { + local repo="$1" + debug "Checking CodeQL workflow status for $repo" + + # Find CodeQL workflow(s) + local workflows + # shellcheck disable=SC2086 + workflows=$(gh api $GH_HOST_FLAG "/repos/$ORG_NAME/$repo/actions/workflows" --jq '.workflows[] | select(.name | test("codeql|CodeQL"; "i")) | .id' 2>/dev/null) + + if [ -z "$workflows" ]; then + echo "No workflow" + return + fi + + # Check the most recent run for each CodeQL workflow + local has_failure=false + local has_success=false + local last_conclusion="" + + for workflow_id in $workflows; do + # shellcheck disable=SC2086 + local run_info + run_info=$(gh api $GH_HOST_FLAG "/repos/$ORG_NAME/$repo/actions/workflows/$workflow_id/runs?per_page=1&branch=main" --jq '.workflow_runs[0] | {conclusion, status}' 2>/dev/null) + + if [ -n "$run_info" ]; then + local conclusion + conclusion=$(echo "$run_info" | jq -r '.conclusion // empty' 2>/dev/null) + + if [ "$conclusion" = "failure" ]; then + has_failure=true + last_conclusion="failure" + elif [ "$conclusion" = "success" ]; then + has_success=true + if [ -z "$last_conclusion" ]; then + last_conclusion="success" + fi + fi + fi + done + + if [ "$has_failure" = true ]; then + echo "Failing" + elif [ "$has_success" = true ]; then + echo "OK" + else + echo "Unknown" + fi +} + +# Function to check if .github/workflows directory exists +has_github_workflows() { + local repo="$1" + debug "Checking for .github/workflows in $repo" + + local response + # shellcheck disable=SC2086 + response=$(gh api $GH_HOST_FLAG "/repos/$ORG_NAME/$repo/contents/.github/workflows" 2>&1) + + # If we get an array response, workflows exist + if echo "$response" | jq -e 'type == "array"' 2>/dev/null | grep -q true; then + echo "true" + else + echo "false" + fi +} + +# Function to get CodeQL-supported languages that are NOT being scanned +get_unscanned_codeql_languages() { + local repo_languages="$1" + local scanned_languages="$2" + local has_workflows="$3" + + local unscanned="" + local scanned_lower + scanned_lower=$(to_lowercase "$scanned_languages") + + # Check if actions workflows exist but aren't being scanned + if [ "$has_workflows" = "true" ]; then + if [[ ! "$scanned_lower" =~ actions ]]; then + unscanned="actions" + fi + fi + + # If no languages detected + if [ -z "$repo_languages" ]; then + if [ -n "$unscanned" ]; then + echo "$unscanned" + elif [ -n "$scanned_languages" ]; then + echo "None" + else + echo "N/A" + fi + return + fi + + IFS=';' read -ra LANGS <<< "$repo_languages" + for lang in "${LANGS[@]}"; do + lang=$(echo "$lang" | sed 's/^ *//;s/ *$//') + local lang_lower + lang_lower=$(to_lowercase "$lang") + + # Normalize C# to csharp for comparison + if [ "$lang_lower" = "c#" ]; then + lang_lower="csharp" + fi + + # Check if this language is CodeQL-supported + if is_codeql_language "$lang_lower"; then + # Check if it's being scanned (case-insensitive) + # Handle CodeQL combined languages: javascript-typescript, java-kotlin + # Note: "javascript" alone covers both JS and TS, same for "java" covering Kotlin + local is_scanned=0 + local unscanned_name="$lang" + + if [[ "$scanned_lower" =~ $lang_lower ]]; then + is_scanned=1 + elif [ "$lang_lower" = "csharp" ]; then + # Normalize C# to csharp for output + if [[ "$scanned_lower" =~ csharp ]]; then + is_scanned=1 + else + unscanned_name="csharp" + fi + elif [ "$lang_lower" = "javascript" ] || [ "$lang_lower" = "typescript" ]; then + # CodeQL's javascript extractor handles both JS and TS + if [[ "$scanned_lower" =~ javascript ]]; then + is_scanned=1 + else + # Normalize to CodeQL language name + unscanned_name="javascript-typescript" + fi + elif [ "$lang_lower" = "java" ] || [ "$lang_lower" = "kotlin" ]; then + # CodeQL's java extractor handles both Java and Kotlin + if [[ "$scanned_lower" =~ java ]]; then + is_scanned=1 + else + # Normalize to CodeQL language name + unscanned_name="java-kotlin" + fi + elif [ "$lang_lower" = "c" ] || [ "$lang_lower" = "c++" ] || [ "$lang_lower" = "cpp" ]; then + # CodeQL's cpp extractor handles both C and C++ + if [[ "$scanned_lower" =~ c-cpp ]] || [[ "$scanned_lower" =~ cpp ]]; then + is_scanned=1 + else + # Normalize to CodeQL language name + unscanned_name="c-cpp" + fi + fi + + if [ "$is_scanned" -eq 0 ]; then + # Avoid duplicates (e.g., if both JavaScript and TypeScript are in repo) + if [ -z "$unscanned" ]; then + unscanned="$unscanned_name" + elif [[ ! ";$unscanned;" =~ ";$unscanned_name;" ]]; then + unscanned="$unscanned;$unscanned_name" + fi + fi + fi + done + + if [ -z "$unscanned" ]; then + echo "None" + else + echo "$unscanned" + fi +} + +# CSV header (conditionally include Workflow Status column) +if [ "$CHECK_WORKFLOWS" -eq 1 ]; then + CSV_HEADER="Repository,Default Branch,Last Updated,Languages,CodeQL Enabled,Last Default Branch Scan Date,Scanned Languages,Unscanned CodeQL Languages,Open Alerts,Analysis Errors,Analysis Warnings,Workflow Status" +else + CSV_HEADER="Repository,Default Branch,Last Updated,Languages,CodeQL Enabled,Last Default Branch Scan Date,Scanned Languages,Unscanned CodeQL Languages,Open Alerts,Analysis Errors,Analysis Warnings" +fi + +# Output function +output_line() { + local line="$1" + if [ -n "$OUTPUT_FILE" ]; then + echo "$line" >> "$OUTPUT_FILE" + else + echo "$line" + fi +} + +# Initialize output file if specified +if [ -n "$OUTPUT_FILE" ]; then + > "$OUTPUT_FILE" +fi + +output_line "$CSV_HEADER" + +debug "Fetching repositories for organization: $ORG_NAME" + +# Fetch repositories - either single repo or all repos in org +total_repos=0 + +if [ -n "$REPO_NAME" ]; then + # Single repo mode - fetch just that repo's info + echo "Generating code scanning coverage report for: $ORG_NAME/$REPO_NAME" >&2 + + # shellcheck disable=SC2086 + repo_info=$(gh api graphql $GH_HOST_FLAG -F org="$ORG_NAME" -F repo="$REPO_NAME" -f query=' + query($org: String!, $repo: String!) { + repository(owner: $org, name: $repo) { + name + updatedAt + defaultBranchRef { + name + } + } + }' --template '{{.data.repository.name}}|{{.data.repository.updatedAt}}|{{if .data.repository.defaultBranchRef}}{{.data.repository.defaultBranchRef.name}}{{else}}main{{end}}') + + if [ -z "$repo_info" ] || [ "$repo_info" = "||" ]; then + error "Repository $ORG_NAME/$REPO_NAME not found or not accessible" + fi + + repos="$repo_info" +else + # All repos mode + echo "Generating code scanning coverage report for: $ORG_NAME" >&2 + + # shellcheck disable=SC2086 + repos=$(gh api graphql $GH_HOST_FLAG --paginate -F org="$ORG_NAME" -f query=' +query($org: String!, $endCursor: String) { + organization(login: $org) { + repositories(first: 100, after: $endCursor) { + pageInfo { + hasNextPage + endCursor + } + nodes { + name + updatedAt + defaultBranchRef { + name + } + } + } + } +}' --template '{{range .data.organization.repositories.nodes}}{{.name}}|{{.updatedAt}}|{{if .defaultBranchRef}}{{.defaultBranchRef.name}}{{else}}main{{end}}{{"\n"}}{{end}}') + + # If sample mode, randomly select SAMPLE_SIZE repos + if [ "$SAMPLE_MODE" -eq 1 ]; then + total_available=$(echo "$repos" | grep -c .) + echo "Sample mode: selecting $SAMPLE_SIZE random repos from $total_available available" >&2 + repos=$(echo "$repos" | sort -R | head -n "$SAMPLE_SIZE") + fi +fi + +while IFS='|' read -r repo_name repo_updated_raw default_branch; do + if [ -z "$repo_name" ]; then + continue + fi + + repo_updated=$(echo "$repo_updated_raw" | cut -d'T' -f1) + # Default to 'main' if no default branch is set (empty repos) + if [ -z "$default_branch" ]; then + default_branch="main" + fi + + total_repos=$((total_repos + 1)) + echo "[$total_repos] Processing: $repo_name (default branch: $default_branch)" >&2 + + # Get repository languages + languages=$(get_repo_languages "$repo_name") + + # Check CodeQL status + codeql_status=$(check_codeql_status "$repo_name") + + # Get scanning info (date, scanned languages, errors, and warnings) for default branch + scanning_info=$(get_scanning_info "$repo_name" "$default_branch") + IFS='|' read -r last_scan_date tool_name scanned_languages analysis_error analysis_warning <<< "$scanning_info" + + # Format last scan date, analysis error, and warning + if [ "$last_scan_date" = "NO_RESULTS" ] || [ -z "$last_scan_date" ]; then + last_scan_display="Never" + scanned_languages="" + analysis_error="" + analysis_warning="" + else + last_scan_display=$(echo "$last_scan_date" | cut -d'T' -f1) + # Format analysis error + if [ -z "$analysis_error" ]; then + analysis_error="None" + fi + # Format analysis warning + if [ -z "$analysis_warning" ]; then + analysis_warning="None" + fi + fi + + # Check for .github/workflows if --check-actions is enabled + has_workflows="false" + if [ "$CHECK_ACTIONS" -eq 1 ]; then + has_workflows=$(has_github_workflows "$repo_name") + fi + + # Get unscanned CodeQL languages + unscanned=$(get_unscanned_codeql_languages "$languages" "$scanned_languages" "$has_workflows") + + # Get open alerts count + open_alerts=$(get_open_alerts_count "$repo_name") + + # Build CSV line (no quotes except for error/warning fields which may contain commas) + if [ "$CHECK_WORKFLOWS" -eq 1 ]; then + # Get CodeQL workflow status (only when --check-workflows is enabled) + workflow_status=$(get_codeql_workflow_status "$repo_name") + csv_line="$repo_name,$default_branch,$repo_updated,$languages,$codeql_status,$last_scan_display,$scanned_languages,$unscanned,$open_alerts,\"$analysis_error\",\"$analysis_warning\",$workflow_status" + else + csv_line="$repo_name,$default_branch,$repo_updated,$languages,$codeql_status,$last_scan_display,$scanned_languages,$unscanned,$open_alerts,\"$analysis_error\",\"$analysis_warning\"" + fi + output_line "$csv_line" + +done <<< "$repos" + +echo "" >&2 +echo "Report complete. Processed $total_repos repositories." >&2 + +if [ -n "$OUTPUT_FILE" ]; then + echo "Report saved to: $OUTPUT_FILE" >&2 + + # Generate sub-reports for actionable items + # Get the base name without extension for sub-reports + base_name="${OUTPUT_FILE%.csv}" + + # Sub-report 1: Repos with disabled CodeQL (Disabled, No, Requires GHAS, No Scans) + disabled_report="${base_name}-disabled.csv" + echo "$CSV_HEADER" > "$disabled_report" + # Column 5 is CodeQL Enabled + awk -F',' 'NR>1 && ($5 ~ /Disabled|^No$|Requires GHAS|No Scans/) {print $0}' "$OUTPUT_FILE" >> "$disabled_report" + disabled_count=$(($(wc -l < "$disabled_report") - 1)) + echo " - Disabled/Not scanning: $disabled_report ($disabled_count repos)" >&2 + + # Sub-report 2: Repos with stale scans (repo modified more than 90 days after last scan) + stale_report="${base_name}-stale.csv" + echo "$CSV_HEADER" > "$stale_report" + # Column 3 is Last Updated, Column 6 is Last Default Branch Scan Date + # Stale = repo was modified more than 90 days after the last scan + awk -F',' 'NR>1 && $6 != "Never" && $6 != "" { + last_updated = $3 + last_scan = $6 + if (last_updated != "" && last_scan != "") { + # Add 90 days to last_scan and compare with last_updated + split(last_scan, d, "-") + scan_year = d[1]; scan_month = d[2]; scan_day = d[3] + # Add 90 days (approximate as 3 months) + scan_month += 3 + if (scan_month > 12) { scan_month -= 12; scan_year += 1 } + cutoff = sprintf("%04d-%02d-%02d", scan_year, scan_month, scan_day) + if (last_updated > cutoff) print $0 + } + }' "$OUTPUT_FILE" >> "$stale_report" + stale_count=$(($(wc -l < "$stale_report") - 1)) + echo " - Stale scans (modified >90 days after scan): $stale_report ($stale_count repos)" >&2 + + # Sub-report 3: Repos with missing CodeQL languages (only if already scanning something) + missing_langs_report="${base_name}-missing-languages.csv" + echo "$CSV_HEADER" > "$missing_langs_report" + # Column 5 is CodeQL Enabled (must be "Yes"), Column 8 is Unscanned CodeQL Languages + # Only include repos that are actively scanning but missing some languages + awk -F',' 'NR>1 && $5 == "Yes" && $8 != "" && $8 != "None" && $8 != "N/A" {print $0}' "$OUTPUT_FILE" >> "$missing_langs_report" + missing_count=$(($(wc -l < "$missing_langs_report") - 1)) + echo " - Missing CodeQL languages: $missing_langs_report ($missing_count repos)" >&2 + + # Sub-report 4: Repos with open alerts + alerts_report="${base_name}-open-alerts.csv" + echo "$CSV_HEADER" > "$alerts_report" + # Column 9 is Open Alerts - filter where > 0 + awk -F',' 'NR>1 && $9 ~ /^[0-9]+$/ && $9 > 0 {print $0}' "$OUTPUT_FILE" >> "$alerts_report" + alerts_count=$(($(wc -l < "$alerts_report") - 1)) + echo " - Repos with open alerts: $alerts_report ($alerts_count repos)" >&2 + + # Sub-report 5: Repos with analysis errors or warnings + errors_report="${base_name}-analysis-issues.csv" + echo "$CSV_HEADER" > "$errors_report" + # Column 10 is Analysis Errors (quoted), Column 11 is Analysis Warnings (quoted) + # Filter where not "None" and not empty (accounting for quotes) + awk -F',' 'NR>1 { + err = $10; warn = $11 + gsub(/"/, "", err); gsub(/"/, "", warn) + if ((err != "" && err != "None") || (warn != "" && warn != "None")) print $0 + }' "$OUTPUT_FILE" >> "$errors_report" + errors_count=$(($(wc -l < "$errors_report") - 1)) + echo " - Analysis errors/warnings: $errors_report ($errors_count repos)" >&2 +fi From 018fb03294c6a241eac29eecc107345e76f086fe Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Mon, 5 Jan 2026 15:55:18 -0600 Subject: [PATCH 02/27] feat: add code scanning coverage report as node script --- scripts/README.md | 4 + .../README.md | 147 ++++ .../get-code-scanning-coverage-report.js | 718 ++++++++++++++++++ .../package-lock.json | 438 +++++++++++ .../package.json | 33 + 5 files changed, 1340 insertions(+) create mode 100644 scripts/get-code-scanning-coverage-report/README.md create mode 100644 scripts/get-code-scanning-coverage-report/get-code-scanning-coverage-report.js create mode 100644 scripts/get-code-scanning-coverage-report/package-lock.json create mode 100644 scripts/get-code-scanning-coverage-report/package.json diff --git a/scripts/README.md b/scripts/README.md index e818d88..30e2a23 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -70,6 +70,10 @@ Delete branch protection rules programmatically based on a pattern. Clean up Azure Storage Account Containers from GEI migrations. +## get-code-scanning-coverage-report + +See: [get-code-scanning-coverage-report](./get-code-scanning-coverage-report/README.md) + ## get-app-tokens-for-each-installation.sh This script will generate generate a JWT for a GitHub app and use that JWT to generate installation tokens for each org installation. The installation tokens, returned as `ghs_abc`, can then be used for normal API calls. It will use the private key and app ID from the GitHub App's settings page and the `get-app-jwt.py` script to generate the JWT, and then use the JWT to generate the installation tokens for each org installation. diff --git a/scripts/get-code-scanning-coverage-report/README.md b/scripts/get-code-scanning-coverage-report/README.md new file mode 100644 index 0000000..cbcc9f1 --- /dev/null +++ b/scripts/get-code-scanning-coverage-report/README.md @@ -0,0 +1,147 @@ +# get-code-scanning-coverage-report + +Generate a comprehensive code scanning coverage report for all repositories in a GitHub organization. + +## Features + +- Reports CodeQL enablement status, last scan date, and scanned languages +- Identifies CodeQL-supported languages that are not being scanned +- Shows open alert counts and analysis errors/warnings +- Generates actionable sub-reports for remediation +- Supports parallel API calls for faster processing +- Works with GitHub.com and GitHub Enterprise Server + +## Prerequisites + +- Node.js 18 or later +- A GitHub token with `repo` scope + +## Installation + +```shell +cd scripts/get-code-scanning-coverage-report +npm install +``` + +## Usage + +```shell +# Set your GitHub token +export GITHUB_TOKEN=ghp_xxxxxxxxxxxx + +# Note: If you are authenticated with the GitHub CLI, you can use `gh auth token` to get your token: +# export GITHUB_TOKEN=$(gh auth token) + +# Basic usage - output to stdout +node get-code-scanning-coverage-report.js my-org + +# Output to file (also generates sub-reports) +node get-code-scanning-coverage-report.js my-org --output report.csv + +# Check a single repository +node get-code-scanning-coverage-report.js my-org --repo my-repo + +# Sample 25 random repositories +node get-code-scanning-coverage-report.js my-org --sample --output sample.csv + +# Include workflow status column +node get-code-scanning-coverage-report.js my-org --check-workflows --output report.csv + +# Check for unscanned GitHub Actions workflows +node get-code-scanning-coverage-report.js my-org --check-actions --output report.csv + +# Use with GitHub Enterprise Server +export GITHUB_API_URL=https://github.example.com/api/v3 +node get-code-scanning-coverage-report.js my-org --output report.csv + +# Adjust concurrency (default: 10) +node get-code-scanning-coverage-report.js my-org --concurrency 5 --output report.csv +``` + +## Options + +| Option | Description | +|--------|-------------| +| `--output ` | Write CSV to file (also generates sub-reports) | +| `--repo ` | Check a single repository instead of all repos | +| `--sample` | Sample 25 random repositories | +| `--check-workflows` | Include CodeQL workflow run status column | +| `--check-actions` | Check for unscanned GitHub Actions workflows | +| `--concurrency ` | Number of concurrent API calls (default: 10) | +| `--help` | Show help message | + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `GITHUB_TOKEN` | GitHub token with `repo` scope (required) | +| `GITHUB_API_URL` | API endpoint (defaults to `https://api.github.com`) | + +## Output Columns + +| Column | Description | +|--------|-------------| +| Repository | Repository name | +| Default Branch | The default branch of the repository | +| Last Updated | When the repository was last updated | +| Languages | Languages detected in the repository (semicolon-separated) | +| CodeQL Enabled | `Yes`, `No Scans`, `Disabled`, `Requires GHAS`, or `No` | +| Last Default Branch Scan Date | Date of most recent scan on default branch | +| Scanned Languages | Languages scanned by CodeQL (semicolon-separated) | +| Unscanned CodeQL Languages | CodeQL-supported languages not being scanned | +| Open Alerts | Number of open code scanning alerts | +| Analysis Errors | Errors from most recent analysis | +| Analysis Warnings | Warnings from most recent analysis | +| Workflow Status | (with `--check-workflows`) `OK`, `Failing`, `No workflow`, or `Unknown` | + +## Sub-reports + +When using `--output`, the script generates actionable sub-reports: + +| File | Description | +|------|-------------| +| `*-disabled.csv` | Repos with CodeQL disabled or no scans | +| `*-stale.csv` | Repos modified >90 days after last scan | +| `*-missing-languages.csv` | Repos scanning but missing some CodeQL languages | +| `*-open-alerts.csv` | Repos with open code scanning alerts | +| `*-analysis-issues.csv` | Repos with analysis errors or warnings | + +## CodeQL Supported Languages + +The script recognizes these CodeQL-supported languages: + +- C/C++ (reported as `c-cpp`) +- C# (reported as `csharp`) +- Go +- Java/Kotlin (reported as `java-kotlin`) +- JavaScript/TypeScript (reported as `javascript-typescript`) +- Python +- Ruby +- Swift + +## Example Output + +```csv +Repository,Default Branch,Last Updated,Languages,CodeQL Enabled,Last Default Branch Scan Date,Scanned Languages,Unscanned CodeQL Languages,Open Alerts,Analysis Errors,Analysis Warnings +my-app,main,2025-12-01,JavaScript;TypeScript;Python,Yes,2025-12-15,javascript-typescript;python,None,3,"None","None" +legacy-service,master,2024-06-15,Java,Yes,2024-01-10,java-kotlin,None,0,"None","None" +new-project,main,2025-12-20,Go;Python,No Scans,Never,,go;python,N/A,"","" +``` + + diff --git a/scripts/get-code-scanning-coverage-report/get-code-scanning-coverage-report.js b/scripts/get-code-scanning-coverage-report/get-code-scanning-coverage-report.js new file mode 100644 index 0000000..d3ac6e6 --- /dev/null +++ b/scripts/get-code-scanning-coverage-report/get-code-scanning-coverage-report.js @@ -0,0 +1,718 @@ +#!/usr/bin/env node + +// +// Generate a comprehensive code scanning coverage report for all repositories in a GitHub organization +// +// Usage: +// node get-code-scanning-coverage-report.js [options] +// +// Options: +// --output Write CSV to file (also generates sub-reports) +// --repo Check a single repository instead of all repos +// --sample Sample 25 random repositories +// --check-workflows Include CodeQL workflow run status column +// --check-actions Check for unscanned GitHub Actions workflows +// --concurrency Number of concurrent API calls (default: 10) +// --help Show help +// +// Environment Variables: +// GITHUB_TOKEN GitHub token with repo scope (required) +// GITHUB_API_URL API endpoint (defaults to https://api.github.com) +// +// Example: +// node get-code-scanning-coverage-report.js my-org --output report.csv +// + +const { Octokit } = require("octokit"); +const fs = require('fs'); +const path = require('path'); + +// CodeQL supported languages +const CODEQL_LANGUAGES = new Set([ + 'c', 'c++', 'cpp', 'csharp', 'c#', 'go', 'java', 'kotlin', + 'javascript', 'typescript', 'python', 'ruby', 'swift' +]); + +// Language normalization map (GitHub language -> CodeQL language name) +const LANGUAGE_NORMALIZE = { + 'c#': 'csharp', + 'csharp': 'csharp', + 'c': 'c-cpp', + 'c++': 'c-cpp', + 'cpp': 'c-cpp', + 'javascript': 'javascript-typescript', + 'typescript': 'javascript-typescript', + 'java': 'java-kotlin', + 'kotlin': 'java-kotlin' +}; + +// Configuration +const SAMPLE_SIZE = 25; +const DEFAULT_CONCURRENCY = 10; + +// Parse command line arguments +function parseArgs() { + const args = process.argv.slice(2); + const config = { + org: null, + output: null, + repo: null, + sample: false, + checkWorkflows: false, + checkActions: false, + concurrency: DEFAULT_CONCURRENCY, + help: false + }; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + switch (arg) { + case '--help': + case '-h': + config.help = true; + break; + case '--output': + config.output = args[++i]; + break; + case '--repo': + config.repo = args[++i]; + break; + case '--sample': + config.sample = true; + break; + case '--check-workflows': + config.checkWorkflows = true; + break; + case '--check-actions': + config.checkActions = true; + break; + case '--concurrency': + config.concurrency = parseInt(args[++i], 10) || DEFAULT_CONCURRENCY; + break; + default: + if (!arg.startsWith('-')) { + config.org = arg; + } else { + console.error(`Unknown option: ${arg}`); + process.exit(1); + } + } + } + + return config; +} + +function showHelp() { + console.log(` +Generate a comprehensive code scanning coverage report for all repositories in a GitHub organization. + +Usage: + node get-code-scanning-coverage-report.js [options] + +Arguments: + organization GitHub organization name + +Options: + --output Write CSV to file (also generates sub-reports) + --repo Check a single repository instead of all repos + --sample Sample ${SAMPLE_SIZE} random repositories + --check-workflows Include CodeQL workflow run status column + --check-actions Check for unscanned GitHub Actions workflows + --concurrency Number of concurrent API calls (default: ${DEFAULT_CONCURRENCY}) + --help Show this help message + +Environment Variables: + GITHUB_TOKEN GitHub token with repo scope (required) + GITHUB_API_URL API endpoint (defaults to https://api.github.com) + +Examples: + node get-code-scanning-coverage-report.js my-org + node get-code-scanning-coverage-report.js my-org --output report.csv + node get-code-scanning-coverage-report.js my-org --repo my-repo + node get-code-scanning-coverage-report.js my-org --sample --output sample.csv + node get-code-scanning-coverage-report.js my-org --check-workflows --check-actions + +Output Columns: + - Repository: Repository name + - Default Branch: The default branch of the repository + - Last Updated: When the repository was last updated + - Languages: Languages detected in the repository + - CodeQL Enabled: Yes / No Scans / Disabled / Requires GHAS / No + - Last Default Branch Scan Date: Date of most recent scan on default branch + - Scanned Languages: Languages scanned by CodeQL + - Unscanned CodeQL Languages: CodeQL-supported languages not being scanned + - Open Alerts: Number of open code scanning alerts + - Analysis Errors: Errors from most recent analysis + - Analysis Warnings: Warnings from most recent analysis + - Workflow Status: (with --check-workflows) CodeQL workflow run status + +Sub-reports (generated with --output): + - *-disabled.csv: Repos with CodeQL disabled or no scans + - *-stale.csv: Repos modified >90 days after last scan + - *-missing-languages.csv: Repos scanning but missing some CodeQL languages + - *-open-alerts.csv: Repos with open code scanning alerts + - *-analysis-issues.csv: Repos with analysis errors or warnings +`); +} + +// Initialize Octokit +function createOctokit() { + const token = process.env.GITHUB_TOKEN; + if (!token) { + console.error("ERROR: GITHUB_TOKEN environment variable is required"); + process.exit(1); + } + + const baseUrl = process.env.GITHUB_API_URL || 'https://api.github.com'; + + return new Octokit({ + auth: token, + baseUrl, + throttle: { + onRateLimit: (retryAfter, options, octokit) => { + console.error(`Rate limit hit, retrying after ${retryAfter} seconds...`); + return true; + }, + onSecondaryRateLimit: (retryAfter, options, octokit) => { + console.error(`Secondary rate limit hit, retrying after ${retryAfter} seconds...`); + return true; + } + } + }); +} + +// Fetch all repositories in an organization using GraphQL +async function fetchRepositories(octokit, org) { + const repos = []; + let cursor = null; + + const query = ` + query($org: String!, $cursor: String) { + organization(login: $org) { + repositories(first: 100, after: $cursor) { + pageInfo { + hasNextPage + endCursor + } + nodes { + name + updatedAt + isArchived + defaultBranchRef { + name + } + } + } + } + } + `; + + do { + const result = await octokit.graphql(query, { org, cursor }); + const data = result.organization.repositories; + repos.push(...data.nodes.map(repo => ({ + name: repo.name, + updatedAt: repo.updatedAt, + isArchived: repo.isArchived, + defaultBranch: repo.defaultBranchRef?.name || 'main' + }))); + cursor = data.pageInfo.hasNextPage ? data.pageInfo.endCursor : null; + } while (cursor); + + return repos; +} + +// Fetch a single repository +async function fetchSingleRepository(octokit, org, repoName) { + const query = ` + query($org: String!, $repo: String!) { + repository(owner: $org, name: $repo) { + name + updatedAt + isArchived + defaultBranchRef { + name + } + } + } + `; + + const result = await octokit.graphql(query, { org, repo: repoName }); + const repo = result.repository; + + if (!repo) { + throw new Error(`Repository ${org}/${repoName} not found`); + } + + return [{ + name: repo.name, + updatedAt: repo.updatedAt, + isArchived: repo.isArchived, + defaultBranch: repo.defaultBranchRef?.name || 'main' + }]; +} + +// Fetch repository languages +async function fetchRepoLanguages(octokit, org, repo) { + try { + const { data } = await octokit.rest.repos.listLanguages({ owner: org, repo }); + return Object.keys(data); + } catch { + return []; + } +} + +// Check CodeQL/code scanning status +async function checkCodeQLStatus(octokit, org, repo) { + try { + const { data } = await octokit.rest.codeScanning.listRecentAnalyses({ + owner: org, + repo, + per_page: 1 + }); + + return data.length > 0 ? 'Yes' : 'No Scans'; + } catch (error) { + const message = error.message?.toLowerCase() || ''; + if (message.includes('advanced security must be enabled')) { + return 'Requires GHAS'; + } + if (message.includes('code security must be enabled')) { + return 'Disabled'; + } + if (message.includes('no analysis found')) { + return 'No Scans'; + } + if (error.status === 404 || error.status === 403) { + return 'No'; + } + return 'Unknown'; + } +} + +// Get code scanning analyses for a repo +async function fetchScanningInfo(octokit, org, repo, defaultBranch) { + try { + const { data } = await octokit.rest.codeScanning.listRecentAnalyses({ + owner: org, + repo, + ref: `refs/heads/${defaultBranch}`, + per_page: 100 + }); + + if (!data || data.length === 0) { + return { + lastScanDate: null, + scannedLanguages: [], + analysisError: null, + analysisWarning: null + }; + } + + // Most recent analysis + const latest = data[0]; + + // Extract unique scanned languages from category field + const languages = new Set(); + for (const analysis of data) { + const category = analysis.category || ''; + const match = category.match(/language:([a-zA-Z0-9_-]+)/); + if (match) { + languages.add(match[1]); + } + } + + return { + lastScanDate: latest.created_at, + scannedLanguages: Array.from(languages), + analysisError: latest.error || null, + analysisWarning: latest.warning || null + }; + } catch { + return { + lastScanDate: null, + scannedLanguages: [], + analysisError: null, + analysisWarning: null + }; + } +} + +// Get open alerts count +async function fetchOpenAlertsCount(octokit, org, repo) { + try { + let count = 0; + for await (const response of octokit.paginate.iterator( + octokit.rest.codeScanning.listAlertsForRepo, + { owner: org, repo, state: 'open', per_page: 100 } + )) { + count += response.data.length; + } + return count; + } catch { + return null; + } +} + +// Check if .github/workflows directory exists +async function hasGitHubWorkflows(octokit, org, repo) { + try { + await octokit.rest.repos.getContent({ + owner: org, + repo, + path: '.github/workflows' + }); + return true; + } catch { + return false; + } +} + +// Get CodeQL workflow status +async function fetchCodeQLWorkflowStatus(octokit, org, repo) { + try { + const { data: workflows } = await octokit.rest.actions.listRepoWorkflows({ + owner: org, + repo + }); + + const codeqlWorkflows = workflows.workflows.filter(w => + w.name.toLowerCase().includes('codeql') + ); + + if (codeqlWorkflows.length === 0) { + return 'No workflow'; + } + + let hasFailure = false; + let hasSuccess = false; + + for (const workflow of codeqlWorkflows) { + try { + const { data: runs } = await octokit.rest.actions.listWorkflowRuns({ + owner: org, + repo, + workflow_id: workflow.id, + per_page: 1 + }); + + if (runs.workflow_runs.length > 0) { + const conclusion = runs.workflow_runs[0].conclusion; + if (conclusion === 'failure') hasFailure = true; + if (conclusion === 'success') hasSuccess = true; + } + } catch { + // Skip workflow if we can't fetch runs + } + } + + if (hasFailure) return 'Failing'; + if (hasSuccess) return 'OK'; + return 'Unknown'; + } catch { + return 'Unknown'; + } +} + +// Check if language is CodeQL scannable +function isCodeQLLanguage(lang) { + return CODEQL_LANGUAGES.has(lang.toLowerCase()); +} + +// Get unscanned CodeQL languages +function getUnscannedLanguages(repoLanguages, scannedLanguages, hasWorkflows, checkActions) { + const unscanned = new Set(); + const scannedLower = scannedLanguages.map(l => l.toLowerCase()); + + // Helper to check if a language is covered by scanned languages + // Handles combined extractors like javascript-typescript covering both js and ts + const isLanguageCovered = (langLower) => { + // Check combined extractor coverage first + // javascript-typescript covers both javascript and typescript + // java-kotlin covers both java and kotlin + // c-cpp covers c, c++, cpp + if (langLower === 'javascript' || langLower === 'typescript') { + return scannedLower.some(s => s.includes('javascript')); + } + if (langLower === 'java' || langLower === 'kotlin') { + return scannedLower.some(s => s.includes('java')); + } + if (langLower === 'c' || langLower === 'c++' || langLower === 'cpp') { + return scannedLower.some(s => s === 'c-cpp' || s === 'cpp' || s.includes('c-cpp')); + } + + // Direct/exact match for other languages (csharp, go, python, ruby, swift) + return scannedLower.includes(langLower); + }; + + // Check for actions if enabled + if (checkActions && hasWorkflows && !scannedLower.includes('actions')) { + unscanned.add('actions'); + } + + // No languages detected + if (repoLanguages.length === 0) { + if (unscanned.size > 0) return Array.from(unscanned); + if (scannedLanguages.length > 0) return []; + return null; // N/A + } + + for (const lang of repoLanguages) { + const langLower = lang.toLowerCase(); + + // Normalize C# to csharp + const normalizedLang = langLower === 'c#' ? 'csharp' : langLower; + + if (!isCodeQLLanguage(normalizedLang)) continue; + + // Check if this language is already being scanned + if (isLanguageCovered(normalizedLang)) continue; + + // Not covered - add normalized CodeQL language name to unscanned list + const unscannedName = LANGUAGE_NORMALIZE[normalizedLang] || normalizedLang; + unscanned.add(unscannedName); + } + + return unscanned.size > 0 ? Array.from(unscanned) : []; +} + +// Process a single repository +async function processRepository(octokit, org, repo, config) { + const [languages, codeqlStatus, scanningInfo, openAlerts, hasWorkflows, workflowStatus] = await Promise.all([ + fetchRepoLanguages(octokit, org, repo.name), + checkCodeQLStatus(octokit, org, repo.name), + fetchScanningInfo(octokit, org, repo.name, repo.defaultBranch), + fetchOpenAlertsCount(octokit, org, repo.name), + config.checkActions ? hasGitHubWorkflows(octokit, org, repo.name) : Promise.resolve(false), + config.checkWorkflows ? fetchCodeQLWorkflowStatus(octokit, org, repo.name) : Promise.resolve(null) + ]); + + const unscanned = getUnscannedLanguages( + languages, + scanningInfo.scannedLanguages, + hasWorkflows, + config.checkActions + ); + + return { + repository: repo.name, + defaultBranch: repo.defaultBranch, + lastUpdated: repo.updatedAt ? repo.updatedAt.split('T')[0] : '', + isArchived: repo.isArchived || false, + languages: languages.join(';'), + codeqlEnabled: codeqlStatus, + lastScanDate: scanningInfo.lastScanDate ? scanningInfo.lastScanDate.split('T')[0] : 'Never', + scannedLanguages: scanningInfo.scannedLanguages.join(';'), + unscannedLanguages: unscanned === null ? 'N/A' : (unscanned.length === 0 ? 'None' : unscanned.join(';')), + openAlerts: openAlerts === null ? 'N/A' : openAlerts, + analysisError: scanningInfo.analysisError || 'None', + analysisWarning: scanningInfo.analysisWarning || 'None', + workflowStatus + }; +} + +// Process repositories with concurrency control +async function processRepositories(octokit, org, repos, config) { + const results = []; + const total = repos.length; + + // Process in batches for concurrency + for (let i = 0; i < repos.length; i += config.concurrency) { + const batch = repos.slice(i, i + config.concurrency); + const batchResults = await Promise.all( + batch.map((repo, idx) => { + const num = i + idx + 1; + process.stderr.write(`[${num}/${total}] Processing: ${repo.name}\n`); + return processRepository(octokit, org, repo, config); + }) + ); + results.push(...batchResults); + } + + return results; +} + +// Generate CSV output +function generateCSV(results, config) { + const headers = [ + 'Repository', + 'Default Branch', + 'Last Updated', + 'Archived', + 'Languages', + 'CodeQL Enabled', + 'Last Default Branch Scan Date', + 'Scanned Languages', + 'Unscanned CodeQL Languages', + 'Open Alerts', + 'Analysis Errors', + 'Analysis Warnings' + ]; + + if (config.checkWorkflows) { + headers.push('Workflow Status'); + } + + const rows = results.map(r => { + const row = [ + r.repository, + r.defaultBranch, + r.lastUpdated, + r.isArchived ? 'Yes' : 'No', + r.languages, + r.codeqlEnabled, + r.lastScanDate, + r.scannedLanguages, + r.unscannedLanguages, + r.openAlerts, + `"${r.analysisError}"`, + `"${r.analysisWarning}"` + ]; + + if (config.checkWorkflows) { + row.push(r.workflowStatus); + } + + return row.join(','); + }); + + return [headers.join(','), ...rows].join('\n'); +} + +// Generate sub-reports +function generateSubReports(results, outputFile, config) { + const baseName = outputFile.replace(/\.csv$/, ''); + const headers = generateCSV([], config).split('\n')[0]; + + // Helper to write sub-report + const writeSubReport = (filename, filter, description) => { + const filtered = results.filter(filter); + const csv = [headers, ...filtered.map(r => { + const row = [ + r.repository, + r.defaultBranch, + r.lastUpdated, + r.isArchived ? 'Yes' : 'No', + r.languages, + r.codeqlEnabled, + r.lastScanDate, + r.scannedLanguages, + r.unscannedLanguages, + r.openAlerts, + `"${r.analysisError}"`, + `"${r.analysisWarning}"` + ]; + if (config.checkWorkflows) row.push(r.workflowStatus); + return row.join(','); + })].join('\n'); + + fs.writeFileSync(filename, csv); + console.error(` - ${description}: ${filename} (${filtered.length} repos)`); + }; + + // Disabled repos (excluding archived repos since they can't be remediated) + writeSubReport( + `${baseName}-disabled.csv`, + r => !r.isArchived && ['Disabled', 'No', 'Requires GHAS', 'No Scans'].includes(r.codeqlEnabled), + 'Disabled/Not scanning' + ); + + // Stale repos (modified >90 days after last scan) + writeSubReport( + `${baseName}-stale.csv`, + r => { + if (r.lastScanDate === 'Never' || !r.lastUpdated) return false; + const scanDate = new Date(r.lastScanDate); + const cutoffDate = new Date(scanDate); + cutoffDate.setDate(cutoffDate.getDate() + 90); + return new Date(r.lastUpdated) > cutoffDate; + }, + 'Stale scans (modified >90 days after scan)' + ); + + // Missing languages (only if already scanning) + writeSubReport( + `${baseName}-missing-languages.csv`, + r => r.codeqlEnabled === 'Yes' && r.unscannedLanguages !== 'None' && r.unscannedLanguages !== 'N/A', + 'Missing CodeQL languages' + ); + + // Open alerts + writeSubReport( + `${baseName}-open-alerts.csv`, + r => typeof r.openAlerts === 'number' && r.openAlerts > 0, + 'Repos with open alerts' + ); + + // Analysis issues + writeSubReport( + `${baseName}-analysis-issues.csv`, + r => (r.analysisError && r.analysisError !== 'None') || (r.analysisWarning && r.analysisWarning !== 'None'), + 'Analysis errors/warnings' + ); +} + +// Shuffle array (Fisher-Yates) +function shuffle(array) { + const arr = [...array]; + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr; +} + +// Main function +async function main() { + const config = parseArgs(); + + if (config.help) { + showHelp(); + process.exit(0); + } + + if (!config.org) { + console.error("ERROR: Organization name is required"); + console.error("Usage: node get-code-scanning-coverage-report.js [options]"); + console.error("Use --help for more information"); + process.exit(1); + } + + const octokit = createOctokit(); + + // Fetch repositories + console.error(`Generating code scanning coverage report for: ${config.org}`); + let repos; + + if (config.repo) { + repos = await fetchSingleRepository(octokit, config.org, config.repo); + } else { + repos = await fetchRepositories(octokit, config.org); + if (config.sample) { + const totalAvailable = repos.length; + repos = shuffle(repos).slice(0, SAMPLE_SIZE); + console.error(`Sample mode: selecting ${SAMPLE_SIZE} random repos from ${totalAvailable} available`); + } + } + + // Process repositories + const results = await processRepositories(octokit, config.org, repos, config); + + // Generate output + const csv = generateCSV(results, config); + + if (config.output) { + fs.writeFileSync(config.output, csv); + console.error(`\nReport complete. Processed ${results.length} repositories.`); + console.error(`Report saved to: ${config.output}`); + generateSubReports(results, config.output, config); + } else { + console.log(csv); + console.error(`\nReport complete. Processed ${results.length} repositories.`); + } +} + +main().catch(err => { + console.error(`ERROR: ${err.message}`); + process.exit(1); +}); diff --git a/scripts/get-code-scanning-coverage-report/package-lock.json b/scripts/get-code-scanning-coverage-report/package-lock.json new file mode 100644 index 0000000..b0c3dcc --- /dev/null +++ b/scripts/get-code-scanning-coverage-report/package-lock.json @@ -0,0 +1,438 @@ +{ + "name": "get-code-scanning-coverage-report", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "get-code-scanning-coverage-report", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "octokit": "^4.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@octokit/app": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-15.1.6.tgz", + "integrity": "sha512-WELCamoCJo9SN0lf3SWZccf68CF0sBNPQuLYmZ/n87p5qvBJDe9aBtr5dHkh7T9nxWZ608pizwsUbypSzZAiUw==", + "license": "MIT", + "dependencies": { + "@octokit/auth-app": "^7.2.1", + "@octokit/auth-unauthenticated": "^6.1.3", + "@octokit/core": "^6.1.5", + "@octokit/oauth-app": "^7.1.6", + "@octokit/plugin-paginate-rest": "^12.0.0", + "@octokit/types": "^14.0.0", + "@octokit/webhooks": "^13.6.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-7.2.2.tgz", + "integrity": "sha512-p6hJtEyQDCJEPN9ijjhEC/kpFHMHN4Gca9r+8S0S8EJi7NaWftaEmexjxxpT1DFBeJpN4u/5RE22ArnyypupJw==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^8.1.4", + "@octokit/auth-oauth-user": "^5.1.4", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "toad-cache": "^3.7.0", + "universal-github-app-jwt": "^2.2.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.4.tgz", + "integrity": "sha512-71iBa5SflSXcclk/OL3lJzdt4iFs56OJdpBGEBl1wULp7C58uiswZLV6TdRaiAzHP1LT8ezpbHlKuxADb+4NkQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^7.1.5", + "@octokit/auth-oauth-user": "^5.1.4", + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.5.tgz", + "integrity": "sha512-lR00+k7+N6xeECj0JuXeULQ2TSBB/zjTAmNF2+vyGPDEFx1dgk1hTDmL13MjbSmzusuAmuJD8Pu39rjp9jH6yw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^5.1.5", + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.6.tgz", + "integrity": "sha512-/R8vgeoulp7rJs+wfJ2LtXEVC7pjQTIqDab7wPKwVG6+2v/lUnCOub6vaHmysQBbb45FknM3tbHW8TOVqYHxCw==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^7.1.5", + "@octokit/oauth-methods": "^5.1.5", + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-token": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", + "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-unauthenticated": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-6.1.3.tgz", + "integrity": "sha512-d5gWJla3WdSl1yjbfMpET+hUSFCE15qM0KVSB0H1shyuJihf/RL1KqWoZMIaonHvlNojkL9XtLFp8QeLe+1iwA==", + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.6.tgz", + "integrity": "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.2.2", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", + "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-app": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-7.1.6.tgz", + "integrity": "sha512-OMcMzY2WFARg80oJNFwWbY51TBUfLH4JGTy119cqiDawSFXSIBujxmpXiKbGWQlvfn0CxE6f7/+c6+Kr5hI2YA==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^8.1.3", + "@octokit/auth-oauth-user": "^5.1.3", + "@octokit/auth-unauthenticated": "^6.1.2", + "@octokit/core": "^6.1.4", + "@octokit/oauth-authorization-url": "^7.1.1", + "@octokit/oauth-methods": "^5.1.4", + "@types/aws-lambda": "^8.10.83", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-authorization-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz", + "integrity": "sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-5.1.5.tgz", + "integrity": "sha512-Ev7K8bkYrYLhoOSZGVAGsLEscZQyq7XQONCBBAl2JdMg7IT3PQn/y8P0KjloPoYpI5UylqYrLeUcScaYWXwDvw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^7.0.0", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/openapi-webhooks-types": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-11.0.0.tgz", + "integrity": "sha512-ZBzCFj98v3SuRM7oBas6BHZMJRadlnDoeFfvm1olVxZnYeU6Vh97FhPxyS5aLh5pN51GYv2I51l/hVUAVkGBlA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-graphql": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-5.2.4.tgz", + "integrity": "sha512-pLZES1jWaOynXKHOqdnwZ5ULeVR6tVVCMm+AUbp0htdcyXDU95WbkYdU4R2ej1wKj5Tu94Mee2Ne0PjPO9cCyA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-12.0.0.tgz", + "integrity": "sha512-MPd6WK1VtZ52lFrgZ0R2FlaoiWllzgqFHaSZxvp72NmoDeZ0m8GeJdg4oB6ctqMTYyrnDYp592Xma21mrgiyDA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-14.0.0.tgz", + "integrity": "sha512-iQt6ovem4b7zZYZQtdv+PwgbL5VPq37th1m2x2TdkgimIDJpsi2A6Q/OI/23i/hR6z5mL0EgisNR4dcbmckSZQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-7.2.1.tgz", + "integrity": "sha512-wUc3gv0D6vNHpGxSaR3FlqJpTXGWgqmk607N9L3LvPL4QjaxDgX/1nY2mGpT37Khn+nlIXdljczkRnNdTTV3/A==", + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-throttling": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-10.0.0.tgz", + "integrity": "sha512-Kuq5/qs0DVYTHZuBAzCZStCzo2nKvVRo/TDNhCcpC2TKiOGz/DisXMCvjt3/b5kr6SCI1Y8eeeJTHBxxpFvZEg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^6.1.3" + } + }, + "node_modules/@octokit/request": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz", + "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^10.1.4", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^2.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/webhooks": { + "version": "13.9.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-13.9.1.tgz", + "integrity": "sha512-Nss2b4Jyn4wB3EAqAPJypGuCJFalz/ZujKBQQ5934To7Xw9xjf4hkr/EAByxQY7hp7MKd790bWGz7XYSTsHmaw==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-webhooks-types": "11.0.0", + "@octokit/request-error": "^6.1.7", + "@octokit/webhooks-methods": "^5.1.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/webhooks-methods": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-5.1.1.tgz", + "integrity": "sha512-NGlEHZDseJTCj8TMMFehzwa9g7On4KJMPVHDSrHxCQumL6uSQR8wIkP/qesv52fXqV1BPf4pTxwtS31ldAt9Xg==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.159", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.159.tgz", + "integrity": "sha512-SAP22WSGNN12OQ8PlCzGzRCZ7QDCwI85dQZbmpz7+mAk+L7j+wI7qnvmdKh+o7A5LaOp6QnOZ2NJphAZQTTHQg==", + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "license": "Apache-2.0" + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "license": "MIT" + }, + "node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/octokit": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-4.1.4.tgz", + "integrity": "sha512-cRvxRte6FU3vAHRC9+PMSY3D+mRAs2Rd9emMoqp70UGRvJRM3sbAoim2IXRZNNsf8wVfn4sGxVBHRAP+JBVX/g==", + "license": "MIT", + "dependencies": { + "@octokit/app": "^15.1.6", + "@octokit/core": "^6.1.5", + "@octokit/oauth-app": "^7.1.6", + "@octokit/plugin-paginate-graphql": "^5.2.4", + "@octokit/plugin-paginate-rest": "^12.0.0", + "@octokit/plugin-rest-endpoint-methods": "^14.0.0", + "@octokit/plugin-retry": "^7.2.1", + "@octokit/plugin-throttling": "^10.0.0", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "@octokit/webhooks": "^13.8.3" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/universal-github-app-jwt": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz", + "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==", + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + } + } +} diff --git a/scripts/get-code-scanning-coverage-report/package.json b/scripts/get-code-scanning-coverage-report/package.json new file mode 100644 index 0000000..a3798c6 --- /dev/null +++ b/scripts/get-code-scanning-coverage-report/package.json @@ -0,0 +1,33 @@ +{ + "name": "get-code-scanning-coverage-report", + "version": "1.0.0", + "description": "Generate a comprehensive code scanning coverage report for all repositories in a GitHub organization", + "main": "get-code-scanning-coverage-report.js", + "scripts": { + "start": "node get-code-scanning-coverage-report.js", + "help": "node get-code-scanning-coverage-report.js --help" + }, + "keywords": [ + "github", + "code-scanning", + "codeql", + "security", + "coverage", + "report", + "github-api", + "octokit" + ], + "author": "Josh Johanning", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "dependencies": { + "octokit": "^4.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/joshjohanning/github-misc-scripts.git", + "directory": "scripts/get-code-scanning-coverage-report" + } +} From e5b80b38b8f2010578449cec82ab64c0e9761a6c Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Mon, 5 Jan 2026 15:55:38 -0600 Subject: [PATCH 03/27] feat: add archived header to bash script --- gh-cli/get-code-scanning-coverage-report.sh | 45 ++++++++++++--------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/gh-cli/get-code-scanning-coverage-report.sh b/gh-cli/get-code-scanning-coverage-report.sh index 01d9c90..6881467 100755 --- a/gh-cli/get-code-scanning-coverage-report.sh +++ b/gh-cli/get-code-scanning-coverage-report.sh @@ -484,9 +484,9 @@ get_unscanned_codeql_languages() { # CSV header (conditionally include Workflow Status column) if [ "$CHECK_WORKFLOWS" -eq 1 ]; then - CSV_HEADER="Repository,Default Branch,Last Updated,Languages,CodeQL Enabled,Last Default Branch Scan Date,Scanned Languages,Unscanned CodeQL Languages,Open Alerts,Analysis Errors,Analysis Warnings,Workflow Status" + CSV_HEADER="Repository,Default Branch,Last Updated,Archived,Languages,CodeQL Enabled,Last Default Branch Scan Date,Scanned Languages,Unscanned CodeQL Languages,Open Alerts,Analysis Errors,Analysis Warnings,Workflow Status" else - CSV_HEADER="Repository,Default Branch,Last Updated,Languages,CodeQL Enabled,Last Default Branch Scan Date,Scanned Languages,Unscanned CodeQL Languages,Open Alerts,Analysis Errors,Analysis Warnings" + CSV_HEADER="Repository,Default Branch,Last Updated,Archived,Languages,CodeQL Enabled,Last Default Branch Scan Date,Scanned Languages,Unscanned CodeQL Languages,Open Alerts,Analysis Errors,Analysis Warnings" fi # Output function @@ -521,11 +521,12 @@ if [ -n "$REPO_NAME" ]; then repository(owner: $org, name: $repo) { name updatedAt + isArchived defaultBranchRef { name } } - }' --template '{{.data.repository.name}}|{{.data.repository.updatedAt}}|{{if .data.repository.defaultBranchRef}}{{.data.repository.defaultBranchRef.name}}{{else}}main{{end}}') + }' --template '{{.data.repository.name}}|{{.data.repository.updatedAt}}|{{if .data.repository.defaultBranchRef}}{{.data.repository.defaultBranchRef.name}}{{else}}main{{end}}|{{.data.repository.isArchived}}') if [ -z "$repo_info" ] || [ "$repo_info" = "||" ]; then error "Repository $ORG_NAME/$REPO_NAME not found or not accessible" @@ -548,13 +549,14 @@ query($org: String!, $endCursor: String) { nodes { name updatedAt + isArchived defaultBranchRef { name } } } } -}' --template '{{range .data.organization.repositories.nodes}}{{.name}}|{{.updatedAt}}|{{if .defaultBranchRef}}{{.defaultBranchRef.name}}{{else}}main{{end}}{{"\n"}}{{end}}') +}' --template '{{range .data.organization.repositories.nodes}}{{.name}}|{{.updatedAt}}|{{if .defaultBranchRef}}{{.defaultBranchRef.name}}{{else}}main{{end}}|{{.isArchived}}{{"\n"}}{{end}}') # If sample mode, randomly select SAMPLE_SIZE repos if [ "$SAMPLE_MODE" -eq 1 ]; then @@ -564,7 +566,7 @@ query($org: String!, $endCursor: String) { fi fi -while IFS='|' read -r repo_name repo_updated_raw default_branch; do +while IFS='|' read -r repo_name repo_updated_raw default_branch is_archived; do if [ -z "$repo_name" ]; then continue fi @@ -574,6 +576,12 @@ while IFS='|' read -r repo_name repo_updated_raw default_branch; do if [ -z "$default_branch" ]; then default_branch="main" fi + # Normalize archived status to Yes/No + if [ "$is_archived" = "true" ]; then + archived_display="Yes" + else + archived_display="No" + fi total_repos=$((total_repos + 1)) echo "[$total_repos] Processing: $repo_name (default branch: $default_branch)" >&2 @@ -622,9 +630,9 @@ while IFS='|' read -r repo_name repo_updated_raw default_branch; do if [ "$CHECK_WORKFLOWS" -eq 1 ]; then # Get CodeQL workflow status (only when --check-workflows is enabled) workflow_status=$(get_codeql_workflow_status "$repo_name") - csv_line="$repo_name,$default_branch,$repo_updated,$languages,$codeql_status,$last_scan_display,$scanned_languages,$unscanned,$open_alerts,\"$analysis_error\",\"$analysis_warning\",$workflow_status" + csv_line="$repo_name,$default_branch,$repo_updated,$archived_display,$languages,$codeql_status,$last_scan_display,$scanned_languages,$unscanned,$open_alerts,\"$analysis_error\",\"$analysis_warning\",$workflow_status" else - csv_line="$repo_name,$default_branch,$repo_updated,$languages,$codeql_status,$last_scan_display,$scanned_languages,$unscanned,$open_alerts,\"$analysis_error\",\"$analysis_warning\"" + csv_line="$repo_name,$default_branch,$repo_updated,$archived_display,$languages,$codeql_status,$last_scan_display,$scanned_languages,$unscanned,$open_alerts,\"$analysis_error\",\"$analysis_warning\"" fi output_line "$csv_line" @@ -641,21 +649,22 @@ if [ -n "$OUTPUT_FILE" ]; then base_name="${OUTPUT_FILE%.csv}" # Sub-report 1: Repos with disabled CodeQL (Disabled, No, Requires GHAS, No Scans) + # Excludes archived repos since they can't be remediated disabled_report="${base_name}-disabled.csv" echo "$CSV_HEADER" > "$disabled_report" - # Column 5 is CodeQL Enabled - awk -F',' 'NR>1 && ($5 ~ /Disabled|^No$|Requires GHAS|No Scans/) {print $0}' "$OUTPUT_FILE" >> "$disabled_report" + # Column 4 is Archived, Column 6 is CodeQL Enabled + awk -F',' 'NR>1 && $4 != "Yes" && ($6 ~ /Disabled|^No$|Requires GHAS|No Scans/) {print $0}' "$OUTPUT_FILE" >> "$disabled_report" disabled_count=$(($(wc -l < "$disabled_report") - 1)) echo " - Disabled/Not scanning: $disabled_report ($disabled_count repos)" >&2 # Sub-report 2: Repos with stale scans (repo modified more than 90 days after last scan) stale_report="${base_name}-stale.csv" echo "$CSV_HEADER" > "$stale_report" - # Column 3 is Last Updated, Column 6 is Last Default Branch Scan Date + # Column 3 is Last Updated, Column 7 is Last Default Branch Scan Date # Stale = repo was modified more than 90 days after the last scan - awk -F',' 'NR>1 && $6 != "Never" && $6 != "" { + awk -F',' 'NR>1 && $7 != "Never" && $7 != "" { last_updated = $3 - last_scan = $6 + last_scan = $7 if (last_updated != "" && last_scan != "") { # Add 90 days to last_scan and compare with last_updated split(last_scan, d, "-") @@ -673,27 +682,27 @@ if [ -n "$OUTPUT_FILE" ]; then # Sub-report 3: Repos with missing CodeQL languages (only if already scanning something) missing_langs_report="${base_name}-missing-languages.csv" echo "$CSV_HEADER" > "$missing_langs_report" - # Column 5 is CodeQL Enabled (must be "Yes"), Column 8 is Unscanned CodeQL Languages + # Column 6 is CodeQL Enabled (must be "Yes"), Column 9 is Unscanned CodeQL Languages # Only include repos that are actively scanning but missing some languages - awk -F',' 'NR>1 && $5 == "Yes" && $8 != "" && $8 != "None" && $8 != "N/A" {print $0}' "$OUTPUT_FILE" >> "$missing_langs_report" + awk -F',' 'NR>1 && $6 == "Yes" && $9 != "" && $9 != "None" && $9 != "N/A" {print $0}' "$OUTPUT_FILE" >> "$missing_langs_report" missing_count=$(($(wc -l < "$missing_langs_report") - 1)) echo " - Missing CodeQL languages: $missing_langs_report ($missing_count repos)" >&2 # Sub-report 4: Repos with open alerts alerts_report="${base_name}-open-alerts.csv" echo "$CSV_HEADER" > "$alerts_report" - # Column 9 is Open Alerts - filter where > 0 - awk -F',' 'NR>1 && $9 ~ /^[0-9]+$/ && $9 > 0 {print $0}' "$OUTPUT_FILE" >> "$alerts_report" + # Column 10 is Open Alerts - filter where > 0 + awk -F',' 'NR>1 && $10 ~ /^[0-9]+$/ && $10 > 0 {print $0}' "$OUTPUT_FILE" >> "$alerts_report" alerts_count=$(($(wc -l < "$alerts_report") - 1)) echo " - Repos with open alerts: $alerts_report ($alerts_count repos)" >&2 # Sub-report 5: Repos with analysis errors or warnings errors_report="${base_name}-analysis-issues.csv" echo "$CSV_HEADER" > "$errors_report" - # Column 10 is Analysis Errors (quoted), Column 11 is Analysis Warnings (quoted) + # Column 11 is Analysis Errors (quoted), Column 12 is Analysis Warnings (quoted) # Filter where not "None" and not empty (accounting for quotes) awk -F',' 'NR>1 { - err = $10; warn = $11 + err = $11; warn = $12 gsub(/"/, "", err); gsub(/"/, "", warn) if ((err != "" && err != "None") || (warn != "" && warn != "None")) print $0 }' "$OUTPUT_FILE" >> "$errors_report" From ce85343b08436c487c81edb06d52c9761e3ccd65 Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Mon, 5 Jan 2026 15:58:50 -0600 Subject: [PATCH 04/27] fix(docs): alphabetization --- scripts/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/README.md b/scripts/README.md index 30e2a23..eb68302 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -70,10 +70,6 @@ Delete branch protection rules programmatically based on a pattern. Clean up Azure Storage Account Containers from GEI migrations. -## get-code-scanning-coverage-report - -See: [get-code-scanning-coverage-report](./get-code-scanning-coverage-report/README.md) - ## get-app-tokens-for-each-installation.sh This script will generate generate a JWT for a GitHub app and use that JWT to generate installation tokens for each org installation. The installation tokens, returned as `ghs_abc`, can then be used for normal API calls. It will use the private key and app ID from the GitHub App's settings page and the `get-app-jwt.py` script to generate the JWT, and then use the JWT to generate the installation tokens for each org installation. @@ -100,6 +96,10 @@ Docs: - [Generating an installation access token for a GitHub App](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app#generating-an-installation-access-token) - [List installations for the authenticated app](https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#list-installations-for-the-authenticated-app) +## get-code-scanning-coverage-report + +See: [get-code-scanning-coverage-report](./get-code-scanning-coverage-report/README.md) + ## get-list-of-resolved-secret-scanning-alerts.sh This script retrieves and lists all resolved secret scanning alerts for a specified GitHub repository. It uses the GitHub API to fetch the alerts and displays them in a tabular format. From 90bea81c2c03e04ba2c81dc49d5bed9517f5cc03 Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Mon, 5 Jan 2026 17:01:17 -0600 Subject: [PATCH 05/27] feat: add GitHub App authentication support and update README --- .../README.md | 53 ++++++++++++++++- .../get-code-scanning-coverage-report.js | 59 +++++++++++++++++-- .../package.json | 1 + 3 files changed, 106 insertions(+), 7 deletions(-) diff --git a/scripts/get-code-scanning-coverage-report/README.md b/scripts/get-code-scanning-coverage-report/README.md index cbcc9f1..ee4eaa8 100644 --- a/scripts/get-code-scanning-coverage-report/README.md +++ b/scripts/get-code-scanning-coverage-report/README.md @@ -14,7 +14,7 @@ Generate a comprehensive code scanning coverage report for all repositories in a ## Prerequisites - Node.js 18 or later -- A GitHub token with `repo` scope +- A GitHub token with `repo` scope, or GitHub App credentials ## Installation @@ -72,11 +72,60 @@ node get-code-scanning-coverage-report.js my-org --concurrency 5 --output report ## Environment Variables +Two authentication methods are supported: + +- **Personal Access Token (PAT)**: Simple setup, good for testing or small organizations +- **GitHub App**: Recommended for production use - provides higher rate limits (5,000 vs 15,000 requests/hour) + +### Token Authentication + | Variable | Description | |----------|-------------| -| `GITHUB_TOKEN` | GitHub token with `repo` scope (required) | +| `GITHUB_TOKEN` | GitHub token with `repo` scope | | `GITHUB_API_URL` | API endpoint (defaults to `https://api.github.com`) | +### GitHub App Authentication (recommended) + +| Variable | Description | +|----------|-------------| +| `GITHUB_APP_ID` | GitHub App ID | +| `GITHUB_APP_PRIVATE_KEY_PATH` | Path to GitHub App private key file (.pem) | +| `GITHUB_APP_INSTALLATION_ID` | GitHub App installation ID for the organization | +| `GITHUB_API_URL` | API endpoint (defaults to `https://api.github.com`) | + +**Required GitHub App Permissions:** + +Repository permissions: + +| Permission | Access | Required For | +|------------|--------|--------------| +| Code scanning alerts | Read | Code scanning status, analyses, and alert counts | +| Metadata | Read | Detecting repository languages (automatic) | +| Contents | Read | Checking for workflow files (only if using --check-actions) | +| Actions | Read | Workflow run status (only if using --check-workflows) | + +Organization permissions: + +| Permission | Access | Required For | +|------------|--------|--------------| +| Administration | Read | Listing all repositories in the organization | + +**Note:** The app must be installed on the organization with access to the repositories you want to scan (either "All repositories" or selected repositories). The app can only report on repositories it has been granted access to. + +**Note:** If GitHub App credentials are provided, they take precedence over `GITHUB_TOKEN`. + +### GitHub App Usage Example + +```shell +# Set GitHub App credentials +export GITHUB_APP_ID=123456 +export GITHUB_APP_PRIVATE_KEY_PATH=/path/to/private-key.pem +export GITHUB_APP_INSTALLATION_ID=12345678 + +# Run the report +node get-code-scanning-coverage-report.js my-org --output report.csv +``` + ## Output Columns | Column | Description | diff --git a/scripts/get-code-scanning-coverage-report/get-code-scanning-coverage-report.js b/scripts/get-code-scanning-coverage-report/get-code-scanning-coverage-report.js index d3ac6e6..71a1090 100644 --- a/scripts/get-code-scanning-coverage-report/get-code-scanning-coverage-report.js +++ b/scripts/get-code-scanning-coverage-report/get-code-scanning-coverage-report.js @@ -16,14 +16,20 @@ // --help Show help // // Environment Variables: -// GITHUB_TOKEN GitHub token with repo scope (required) -// GITHUB_API_URL API endpoint (defaults to https://api.github.com) +// GITHUB_TOKEN GitHub PAT with repo scope (required if not using App auth) +// GITHUB_API_URL API endpoint (defaults to https://api.github.com) +// +// GitHub App Authentication (alternative to GITHUB_TOKEN, recommended for higher rate limits): +// GITHUB_APP_ID GitHub App ID +// GITHUB_APP_PRIVATE_KEY_PATH Path to GitHub App private key file (.pem) +// GITHUB_APP_INSTALLATION_ID GitHub App installation ID for the organization // // Example: // node get-code-scanning-coverage-report.js my-org --output report.csv // const { Octokit } = require("octokit"); +const { createAppAuth } = require("@octokit/auth-app"); const fs = require('fs'); const path = require('path'); @@ -157,14 +163,57 @@ Sub-reports (generated with --output): // Initialize Octokit function createOctokit() { + const baseUrl = process.env.GITHUB_API_URL || 'https://api.github.com'; + + // Check for GitHub App authentication + const appId = process.env.GITHUB_APP_ID; + const privateKeyPath = process.env.GITHUB_APP_PRIVATE_KEY_PATH; + const installationId = process.env.GITHUB_APP_INSTALLATION_ID; + + if (appId && privateKeyPath && installationId) { + // Use GitHub App authentication + console.error('Using GitHub App authentication...'); + + // Read private key from file + let privateKey; + try { + privateKey = fs.readFileSync(privateKeyPath, 'utf8'); + } catch (error) { + console.error(`ERROR: Failed to read private key file: ${privateKeyPath}`); + console.error(error.message); + process.exit(1); + } + + return new Octokit({ + authStrategy: createAppAuth, + auth: { + appId: parseInt(appId, 10), + privateKey, + installationId: parseInt(installationId, 10) + }, + baseUrl, + throttle: { + onRateLimit: (retryAfter, options, octokit) => { + console.error(`Rate limit hit, retrying after ${retryAfter} seconds...`); + return true; + }, + onSecondaryRateLimit: (retryAfter, options, octokit) => { + console.error(`Secondary rate limit hit, retrying after ${retryAfter} seconds...`); + return true; + } + } + }); + } + + // Fall back to token authentication const token = process.env.GITHUB_TOKEN; if (!token) { - console.error("ERROR: GITHUB_TOKEN environment variable is required"); + console.error("ERROR: Authentication required. Set either:"); + console.error(" - GITHUB_TOKEN environment variable, or"); + console.error(" - GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY_PATH, and GITHUB_APP_INSTALLATION_ID"); process.exit(1); } - const baseUrl = process.env.GITHUB_API_URL || 'https://api.github.com'; - return new Octokit({ auth: token, baseUrl, diff --git a/scripts/get-code-scanning-coverage-report/package.json b/scripts/get-code-scanning-coverage-report/package.json index a3798c6..6e6e12c 100644 --- a/scripts/get-code-scanning-coverage-report/package.json +++ b/scripts/get-code-scanning-coverage-report/package.json @@ -23,6 +23,7 @@ "node": ">=18.0.0" }, "dependencies": { + "@octokit/auth-app": "^7.0.0", "octokit": "^4.0.0" }, "repository": { From 85252702b2df325288218b7f18c6830d3bb1e788 Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Tue, 6 Jan 2026 08:55:17 -0600 Subject: [PATCH 06/27] refactor: rename script --- scripts/README.md | 4 ++-- .../README.md | 24 +++++++++---------- .../code-scanning-coverage-report.js} | 18 +++++++------- .../package-lock.json | 4 ++-- .../package.json | 10 ++++---- 5 files changed, 30 insertions(+), 30 deletions(-) rename scripts/{get-code-scanning-coverage-report => code-scanning-coverage-report}/README.md (89%) rename scripts/{get-code-scanning-coverage-report/get-code-scanning-coverage-report.js => code-scanning-coverage-report/code-scanning-coverage-report.js} (97%) rename scripts/{get-code-scanning-coverage-report => code-scanning-coverage-report}/package-lock.json (99%) rename scripts/{get-code-scanning-coverage-report => code-scanning-coverage-report}/package.json (68%) diff --git a/scripts/README.md b/scripts/README.md index eb68302..420d594 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -96,9 +96,9 @@ Docs: - [Generating an installation access token for a GitHub App](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app#generating-an-installation-access-token) - [List installations for the authenticated app](https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#list-installations-for-the-authenticated-app) -## get-code-scanning-coverage-report +## code-scanning-coverage-report -See: [get-code-scanning-coverage-report](./get-code-scanning-coverage-report/README.md) +See: [code-scanning-coverage-report](./code-scanning-coverage-report/README.md) ## get-list-of-resolved-secret-scanning-alerts.sh diff --git a/scripts/get-code-scanning-coverage-report/README.md b/scripts/code-scanning-coverage-report/README.md similarity index 89% rename from scripts/get-code-scanning-coverage-report/README.md rename to scripts/code-scanning-coverage-report/README.md index ee4eaa8..9f7a593 100644 --- a/scripts/get-code-scanning-coverage-report/README.md +++ b/scripts/code-scanning-coverage-report/README.md @@ -1,4 +1,4 @@ -# get-code-scanning-coverage-report +# code-scanning-coverage-report Generate a comprehensive code scanning coverage report for all repositories in a GitHub organization. @@ -19,7 +19,7 @@ Generate a comprehensive code scanning coverage report for all repositories in a ## Installation ```shell -cd scripts/get-code-scanning-coverage-report +cd scripts/code-scanning-coverage-report npm install ``` @@ -33,29 +33,29 @@ export GITHUB_TOKEN=ghp_xxxxxxxxxxxx # export GITHUB_TOKEN=$(gh auth token) # Basic usage - output to stdout -node get-code-scanning-coverage-report.js my-org +node code-scanning-coverage-report.js my-org # Output to file (also generates sub-reports) -node get-code-scanning-coverage-report.js my-org --output report.csv +node code-scanning-coverage-report.js my-org --output report.csv # Check a single repository -node get-code-scanning-coverage-report.js my-org --repo my-repo +node code-scanning-coverage-report.js my-org --repo my-repo # Sample 25 random repositories -node get-code-scanning-coverage-report.js my-org --sample --output sample.csv +node code-scanning-coverage-report.js my-org --sample --output sample.csv # Include workflow status column -node get-code-scanning-coverage-report.js my-org --check-workflows --output report.csv +node code-scanning-coverage-report.js my-org --check-workflows --output report.csv # Check for unscanned GitHub Actions workflows -node get-code-scanning-coverage-report.js my-org --check-actions --output report.csv +node code-scanning-coverage-report.js my-org --check-actions --output report.csv # Use with GitHub Enterprise Server export GITHUB_API_URL=https://github.example.com/api/v3 -node get-code-scanning-coverage-report.js my-org --output report.csv +node code-scanning-coverage-report.js my-org --output report.csv # Adjust concurrency (default: 10) -node get-code-scanning-coverage-report.js my-org --concurrency 5 --output report.csv +node code-scanning-coverage-report.js my-org --concurrency 5 --output report.csv ``` ## Options @@ -123,7 +123,7 @@ export GITHUB_APP_PRIVATE_KEY_PATH=/path/to/private-key.pem export GITHUB_APP_INSTALLATION_ID=12345678 # Run the report -node get-code-scanning-coverage-report.js my-org --output report.csv +node code-scanning-coverage-report.js my-org --output report.csv ``` ## Output Columns @@ -180,7 +180,7 @@ new-project,main,2025-12-20,Go;Python,No Scans,Never,,go;python,N/A,"",""