From 3090a30fad265f302d5c12852ae37db63ed66799 Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Tue, 17 Jun 2025 15:47:40 +0100 Subject: [PATCH 1/4] feat(scripts): Coverage script --- scripts/coverage.py | 381 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 381 insertions(+) create mode 100644 scripts/coverage.py diff --git a/scripts/coverage.py b/scripts/coverage.py new file mode 100644 index 0000000..e5ce7eb --- /dev/null +++ b/scripts/coverage.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +""" +Coverage script to load .coverage.json and integrate CodeQL query resolution data. + +This script: +1. Loads the existing .coverage.json file from the project root +2. Runs 'codeql resolve queries --format=json ./ql/src' to get available queries +3. Integrates the query data into the coverage file +""" + +import json +import subprocess +import sys +import argparse +from pathlib import Path +from typing import Dict, List, Any + + +def find_project_root() -> Path: + """Find the project root directory by looking for .coverage.json file.""" + current_dir = Path(__file__).parent + + # Look for .coverage.json in parent directories + while current_dir != current_dir.parent: + coverage_file = current_dir / ".coverage.json" + if coverage_file.exists(): + return current_dir + current_dir = current_dir.parent + + # If not found, assume project root is one level up from scripts directory + return Path(__file__).parent.parent + + +def load_coverage_file(project_root: Path) -> Dict[str, Any]: + """Load the existing .coverage.json file.""" + coverage_file = project_root / ".coverage.json" + + if not coverage_file.exists(): + print(f"Error: .coverage.json not found at {coverage_file}") + sys.exit(1) + + try: + with open(coverage_file, 'r', encoding='utf-8') as f: + return json.load(f) + except json.JSONDecodeError as e: + print(f"Error: Invalid JSON in .coverage.json: {e}") + sys.exit(1) + + +def run_codeql_resolve_queries(project_root: Path) -> List[str]: + """Run codeql resolve queries command and return the list of query paths.""" + ql_src_path = project_root / "ql" / "src" + + if not ql_src_path.exists(): + print(f"Error: ql/src directory not found at {ql_src_path}") + sys.exit(1) + + try: + cmd = ["codeql", "resolve", "queries", "--format=json", str(ql_src_path)] + result = subprocess.run( + cmd, + cwd=project_root, + capture_output=True, + text=True, + check=True + ) + + # Parse the JSON output + queries = json.loads(result.stdout) + return queries + + except subprocess.CalledProcessError as e: + print(f"Error running codeql command: {e}") + print(f"stderr: {e.stderr}") + sys.exit(1) + except json.JSONDecodeError as e: + print(f"Error parsing codeql output as JSON: {e}") + sys.exit(1) + + +def process_query_paths(queries: List[str], project_root: Path) -> List[Dict[str, Any]]: + """Process query paths to extract metadata and create coverage entries.""" + processed_queries = [] + + for query_path in queries: + # Convert absolute path to relative path from project root + try: + relative_path = Path(query_path).relative_to(project_root) + except ValueError: + # If the path is not relative to project root, use the full path + relative_path = Path(query_path) + + # Extract query metadata + query_info = { + "path": str(relative_path), + "absolute_path": query_path, + "name": Path(query_path).stem, + "category": extract_category_from_path(relative_path), + "cwe": extract_cwe_from_path(relative_path), + "covered": False, # Default to not covered + "test_files": [] # Will be populated with test file paths if any + } + + processed_queries.append(query_info) + + return processed_queries + + +def extract_category_from_path(path: Path) -> str: + """Extract category from query path (e.g., 'security', 'diagnostics').""" + parts = path.parts + if len(parts) >= 3 and parts[0] == "ql" and parts[1] == "src": + return parts[2] + return "unknown" + + +def extract_cwe_from_path(path: Path) -> str: + """Extract CWE number from query path if present.""" + parts = path.parts + for part in parts: + if part.startswith("CWE-"): + return part + return "" + + +def update_coverage_file(coverage_data: Dict[str, Any], queries: List[Dict[str, Any]]) -> Dict[str, Any]: + """Update the coverage data with query information.""" + # Add queries to the coverage data + coverage_data["queries"] = queries + + # Update metadata + coverage_data["metadata"] = { + "total_queries": len(queries), + "covered_queries": sum(1 for q in queries if q["covered"]), + "categories": list(set(q["category"] for q in queries)), + "cwes": list(set(q["cwe"] for q in queries if q["cwe"])) + } + + # Calculate coverage percentage + total = coverage_data["metadata"]["total_queries"] + covered = coverage_data["metadata"]["covered_queries"] + coverage_data["metadata"]["coverage_percentage"] = (covered / total * 100) if total > 0 else 0 + + return coverage_data + + +def save_coverage_file(coverage_data: Dict[str, Any], project_root: Path) -> None: + """Save the updated coverage data back to .coverage.json.""" + coverage_file = project_root / ".coverage.json" + + try: + with open(coverage_file, 'w', encoding='utf-8') as f: + json.dump(coverage_data, f, indent=2, ensure_ascii=False) + print(f"Successfully updated {coverage_file}") + except Exception as e: + print(f"Error saving coverage file: {e}") + sys.exit(1) + + +def generate_coverage_markdown(coverage_data: Dict[str, Any]) -> str: + """Generate markdown coverage report from coverage data.""" + metadata = coverage_data["metadata"] + queries = coverage_data["queries"] + + # Calculate coverage percentage + coverage_pct = metadata["coverage_percentage"] + + # Create coverage badge color based on percentage + if coverage_pct >= 80: + badge_color = "brightgreen" + elif coverage_pct >= 60: + badge_color = "yellow" + elif coverage_pct >= 40: + badge_color = "orange" + else: + badge_color = "red" + + # Generate markdown content + md_content = [] + + # Coverage badge + md_content.append(f"![Coverage](https://img.shields.io/badge/Query_Coverage-{coverage_pct:.1f}%25-{badge_color})") + md_content.append("") + + # Summary statistics + md_content.append("| Metric | Value |") + md_content.append("|--------|-------|") + md_content.append(f"| Total Queries | {metadata['total_queries']} |") + md_content.append(f"| Covered Queries | {metadata['covered_queries']} |") + md_content.append(f"| Coverage Percentage | {coverage_pct:.1f}% |") + md_content.append(f"| Categories | {len(metadata['categories'])} |") + md_content.append(f"| CWE Categories | {len(metadata['cwes'])} |") + md_content.append("") + + # Coverage by category + if queries: + category_stats = {} + for query in queries: + category = query["category"] + if category not in category_stats: + category_stats[category] = {"total": 0, "covered": 0} + category_stats[category]["total"] += 1 + if query["covered"]: + category_stats[category]["covered"] += 1 + + md_content.append("### Coverage by Category") + md_content.append("") + md_content.append("| Category | Covered | Total | Percentage |") + md_content.append("|----------|---------|-------|------------|") + + for category in sorted(category_stats.keys()): + stats = category_stats[category] + pct = (stats["covered"] / stats["total"] * 100) if stats["total"] > 0 else 0 + md_content.append(f"| {category.title()} | {stats['covered']} | {stats['total']} | {pct:.1f}% |") + + md_content.append("") + + # CWE coverage breakdown + if metadata["cwes"]: + cwe_stats = {} + for query in queries: + if query["cwe"]: + cwe = query["cwe"] + if cwe not in cwe_stats: + cwe_stats[cwe] = {"total": 0, "covered": 0} + cwe_stats[cwe]["total"] += 1 + if query["covered"]: + cwe_stats[cwe]["covered"] += 1 + + if cwe_stats: + md_content.append("### Coverage by CWE") + md_content.append("") + md_content.append("| CWE | Description | Covered | Total | Percentage |") + md_content.append("|-----|-------------|---------|-------|------------|") + + # CWE descriptions for common ones + cwe_descriptions = { + "CWE-200": "Information Exposure", + "CWE-284": "Improper Access Control", + "CWE-306": "Missing Authentication", + "CWE-319": "Cleartext Transmission", + "CWE-327": "Broken/Risky Crypto Algorithm", + "CWE-352": "Cross-Site Request Forgery", + "CWE-272": "Least Privilege Violation", + "CWE-311": "Missing Encryption", + "CWE-400": "Resource Exhaustion", + "CWE-942": "Overly Permissive CORS", + "CWE-693": "Protection Mechanism Failure", + "CWE-295": "Improper Certificate Validation", + "CWE-798": "Hard-coded Credentials", + "CWE-404": "Improper Resource Shutdown" + } + + for cwe in sorted(cwe_stats.keys()): + stats = cwe_stats[cwe] + pct = (stats["covered"] / stats["total"] * 100) if stats["total"] > 0 else 0 + description = cwe_descriptions.get(cwe, "Security Vulnerability") + md_content.append(f"| {cwe} | {description} | {stats['covered']} | {stats['total']} | {pct:.1f}% |") + + md_content.append("") + + # Last updated timestamp + from datetime import datetime + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC") + md_content.append(f"*Last updated: {timestamp}*") + + return "\n".join(md_content) + + +def update_readme_coverage(project_root: Path, coverage_markdown: str) -> None: + """Update the README.md file with the coverage report.""" + readme_file = project_root / "README.md" + + if not readme_file.exists(): + print(f"Warning: README.md not found at {readme_file}") + return + + try: + with open(readme_file, 'r', encoding='utf-8') as f: + content = f.read() + + # Find the coverage report markers + start_marker = "" + end_marker = "" + + start_idx = content.find(start_marker) + end_idx = content.find(end_marker) + + if start_idx == -1 or end_idx == -1: + print(f"Warning: Coverage report markers not found in {readme_file}") + print("Please add the following markers to your README.md where you want the coverage report:") + print(f" {start_marker}") + print(f" {end_marker}") + return + + # Replace the content between markers + new_content = ( + content[:start_idx + len(start_marker)] + + "\n\n" + coverage_markdown + "\n\n" + + content[end_idx:] + ) + + with open(readme_file, 'w', encoding='utf-8') as f: + f.write(new_content) + + print(f"Successfully updated coverage report in {readme_file}") + + except Exception as e: + print(f"Error updating README.md: {e}") + + +def main(): + """Main function to orchestrate the coverage update process.""" + parser = argparse.ArgumentParser(description="Generate CodeQL query coverage report") + parser.add_argument( + "--markdown-only", + action="store_true", + help="Generate only the markdown report and print to stdout (don't update files)" + ) + parser.add_argument( + "--no-readme-update", + action="store_true", + help="Don't update the README.md file with the coverage report" + ) + + args = parser.parse_args() + + if not args.markdown_only: + print("Loading CodeQL query coverage data...") + + # Find project root + project_root = find_project_root() + if not args.markdown_only: + print(f"Project root: {project_root}") + + # Load existing coverage file + coverage_data = load_coverage_file(project_root) + if not args.markdown_only: + print("Loaded existing coverage data") + + # Run codeql resolve queries + if not args.markdown_only: + print("Running codeql resolve queries...") + query_paths = run_codeql_resolve_queries(project_root) + if not args.markdown_only: + print(f"Found {len(query_paths)} queries") + + # Process query paths + processed_queries = process_query_paths(query_paths, project_root) + + # Update coverage data + updated_coverage = update_coverage_file(coverage_data, processed_queries) + + # Generate markdown coverage report + coverage_markdown = generate_coverage_markdown(updated_coverage) + + if args.markdown_only: + # Just print the markdown and exit + print(coverage_markdown) + return + + # Save updated coverage file + save_coverage_file(updated_coverage, project_root) + + # Update README if not disabled + if not args.no_readme_update: + print("Generating coverage report...") + update_readme_coverage(project_root, coverage_markdown) + + # Print summary + metadata = updated_coverage["metadata"] + print(f"\nCoverage Summary:") + print(f" Total queries: {metadata['total_queries']}") + print(f" Covered queries: {metadata['covered_queries']}") + print(f" Coverage percentage: {metadata['coverage_percentage']:.1f}%") + print(f" Categories: {', '.join(metadata['categories'])}") + print(f" CWEs covered: {len(metadata['cwes'])}") + + +if __name__ == "__main__": + main() \ No newline at end of file From 7fd0a0fd01fc3faec37ff4ddb7ee5b59d22bbfdc Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Tue, 17 Jun 2025 15:48:22 +0100 Subject: [PATCH 2/4] feat: Add coverage data and README update --- .coverage.json | 353 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 46 +++++++ 2 files changed, 399 insertions(+) create mode 100644 .coverage.json diff --git a/.coverage.json b/.coverage.json new file mode 100644 index 0000000..7e84ec4 --- /dev/null +++ b/.coverage.json @@ -0,0 +1,353 @@ +{ + "queries": [ + { + "path": "ql/src/security/CWE-200/PublicResource.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-200/PublicResource.ql", + "name": "PublicResource", + "category": "security", + "cwe": "CWE-200", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-200/GrafanaExternalSnapshotsEnabled.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-200/GrafanaExternalSnapshotsEnabled.ql", + "name": "GrafanaExternalSnapshotsEnabled", + "category": "security", + "cwe": "CWE-200", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-284/DatabasePublicNetworkAccess.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-284/DatabasePublicNetworkAccess.ql", + "name": "DatabasePublicNetworkAccess", + "category": "security", + "cwe": "CWE-284", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-284/RedisCachePublicNetwork.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-284/RedisCachePublicNetwork.ql", + "name": "RedisCachePublicNetwork", + "category": "security", + "cwe": "CWE-284", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-306/RedisCacheNoAuth.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-306/RedisCacheNoAuth.ql", + "name": "RedisCacheNoAuth", + "category": "security", + "cwe": "CWE-306", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-306/GrafanaApiKeyEnabled.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-306/GrafanaApiKeyEnabled.ql", + "name": "GrafanaApiKeyEnabled", + "category": "security", + "cwe": "CWE-306", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-319/GrafanaInsecureStartTLSPolicy.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-319/GrafanaInsecureStartTLSPolicy.ql", + "name": "GrafanaInsecureStartTLSPolicy", + "category": "security", + "cwe": "CWE-319", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-319/DatabaseSslNotEnforced.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-319/DatabaseSslNotEnforced.ql", + "name": "DatabaseSslNotEnforced", + "category": "security", + "cwe": "CWE-319", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-319/SslEnforement.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-319/SslEnforement.ql", + "name": "SslEnforement", + "category": "security", + "cwe": "CWE-319", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-319/RedisCacheNonSslPort.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-319/RedisCacheNonSslPort.ql", + "name": "RedisCacheNonSslPort", + "category": "security", + "cwe": "CWE-319", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-352/GrafanaCsrfDisabled.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-352/GrafanaCsrfDisabled.ql", + "name": "GrafanaCsrfDisabled", + "category": "security", + "cwe": "CWE-352", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-327/DatabaseWeakTlsVersion.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-327/DatabaseWeakTlsVersion.ql", + "name": "DatabaseWeakTlsVersion", + "category": "security", + "cwe": "CWE-327", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-327/WeakTlsVersion.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-327/WeakTlsVersion.ql", + "name": "WeakTlsVersion", + "category": "security", + "cwe": "CWE-327", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-327/TlsDisabled.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-327/TlsDisabled.ql", + "name": "TlsDisabled", + "category": "security", + "cwe": "CWE-327", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/AKS/AKSPublicApi.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/AKS/AKSPublicApi.ql", + "name": "AKSPublicApi", + "category": "security", + "cwe": "", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/AKS/AKSKubeDashboardEnabled.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/AKS/AKSKubeDashboardEnabled.ql", + "name": "AKSKubeDashboardEnabled", + "category": "security", + "cwe": "", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/AKS/AKSPrivateApiEnabled.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/AKS/AKSPrivateApiEnabled.ql", + "name": "AKSPrivateApiEnabled", + "category": "security", + "cwe": "", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/Dashboards/GrafanaMissingZoneRedundancy.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/Dashboards/GrafanaMissingZoneRedundancy.ql", + "name": "GrafanaMissingZoneRedundancy", + "category": "security", + "cwe": "", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/Storage/SupportHttpTraffic.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/Storage/SupportHttpTraffic.ql", + "name": "SupportHttpTraffic", + "category": "security", + "cwe": "", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/Storage/PublicAccess.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/Storage/PublicAccess.ql", + "name": "PublicAccess", + "category": "security", + "cwe": "", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-272/GrafanaExcessiveEditorPermissions.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-272/GrafanaExcessiveEditorPermissions.ql", + "name": "GrafanaExcessiveEditorPermissions", + "category": "security", + "cwe": "CWE-272", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-272/GrafanaExcessiveViewerPermissions.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-272/GrafanaExcessiveViewerPermissions.ql", + "name": "GrafanaExcessiveViewerPermissions", + "category": "security", + "cwe": "CWE-272", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-311/DatabaseNoInfrastructureEncryption.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-311/DatabaseNoInfrastructureEncryption.ql", + "name": "DatabaseNoInfrastructureEncryption", + "category": "security", + "cwe": "CWE-311", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-400/RedisCacheUnsafeMemoryPolicy.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-400/RedisCacheUnsafeMemoryPolicy.ql", + "name": "RedisCacheUnsafeMemoryPolicy", + "category": "security", + "cwe": "CWE-400", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-942/InsecureCorsAllHeaders.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-942/InsecureCorsAllHeaders.ql", + "name": "InsecureCorsAllHeaders", + "category": "security", + "cwe": "CWE-942", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-942/InsecureCorsAllowCredentialsWildcard.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-942/InsecureCorsAllowCredentialsWildcard.ql", + "name": "InsecureCorsAllowCredentialsWildcard", + "category": "security", + "cwe": "CWE-942", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-942/InsecureCorsWildcardOrigin.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-942/InsecureCorsWildcardOrigin.ql", + "name": "InsecureCorsWildcardOrigin", + "category": "security", + "cwe": "CWE-942", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-942/InsecureCorsAllMethods.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-942/InsecureCorsAllMethods.ql", + "name": "InsecureCorsAllMethods", + "category": "security", + "cwe": "CWE-942", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-693/RedisCacheNoBackup.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-693/RedisCacheNoBackup.ql", + "name": "RedisCacheNoBackup", + "category": "security", + "cwe": "CWE-693", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-295/GrafanaSmtpSslVerificationDisabled.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-295/GrafanaSmtpSslVerificationDisabled.ql", + "name": "GrafanaSmtpSslVerificationDisabled", + "category": "security", + "cwe": "CWE-295", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-798/RedisCacheNoAAD.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-798/RedisCacheNoAAD.ql", + "name": "RedisCacheNoAAD", + "category": "security", + "cwe": "CWE-798", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-798/HardcodedSmtpCredentials.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-798/HardcodedSmtpCredentials.ql", + "name": "HardcodedSmtpCredentials", + "category": "security", + "cwe": "CWE-798", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-404/CosmosDBNoBackupPolicy.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-404/CosmosDBNoBackupPolicy.ql", + "name": "CosmosDBNoBackupPolicy", + "category": "security", + "cwe": "CWE-404", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/security/CWE-404/DatabaseNoGeoRedundantBackup.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/security/CWE-404/DatabaseNoGeoRedundantBackup.ql", + "name": "DatabaseNoGeoRedundantBackup", + "category": "security", + "cwe": "CWE-404", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/diagnostics/ExtractionErrors.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/diagnostics/ExtractionErrors.ql", + "name": "ExtractionErrors", + "category": "diagnostics", + "cwe": "", + "covered": false, + "test_files": [] + }, + { + "path": "ql/src/diagnostics/SuccessfullyExtractedFiles.ql", + "absolute_path": "/home/geekmasher/development/github/codeql-extractor-bicep/ql/src/diagnostics/SuccessfullyExtractedFiles.ql", + "name": "SuccessfullyExtractedFiles", + "category": "diagnostics", + "cwe": "", + "covered": false, + "test_files": [] + } + ], + "metadata": { + "total_queries": 36, + "covered_queries": 0, + "categories": [ + "diagnostics", + "security" + ], + "cwes": [ + "CWE-200", + "CWE-306", + "CWE-311", + "CWE-942", + "CWE-400", + "CWE-319", + "CWE-693", + "CWE-327", + "CWE-284", + "CWE-295", + "CWE-404", + "CWE-352", + "CWE-798", + "CWE-272" + ], + "coverage_percentage": 0.0 + } +} \ No newline at end of file diff --git a/README.md b/README.md index 96e4720..cd959b0 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,52 @@ uses: GitHubSecurityLab/codeql-extractor-bicep@v0.1.0 ``` +## Features + +### Coverage + + + +![Coverage](https://img.shields.io/badge/Query_Coverage-0.0%25-red) + +| Metric | Value | +|--------|-------| +| Total Queries | 36 | +| Covered Queries | 0 | +| Coverage Percentage | 0.0% | +| Categories | 2 | +| CWE Categories | 14 | + +### Coverage by Category + +| Category | Covered | Total | Percentage | +|----------|---------|-------|------------| +| Diagnostics | 0 | 2 | 0.0% | +| Security | 0 | 34 | 0.0% | + +### Coverage by CWE + +| CWE | Description | Covered | Total | Percentage | +|-----|-------------|---------|-------|------------| +| CWE-200 | Information Exposure | 0 | 2 | 0.0% | +| CWE-272 | Least Privilege Violation | 0 | 2 | 0.0% | +| CWE-284 | Improper Access Control | 0 | 2 | 0.0% | +| CWE-295 | Improper Certificate Validation | 0 | 1 | 0.0% | +| CWE-306 | Missing Authentication | 0 | 2 | 0.0% | +| CWE-311 | Missing Encryption | 0 | 1 | 0.0% | +| CWE-319 | Cleartext Transmission | 0 | 4 | 0.0% | +| CWE-327 | Broken/Risky Crypto Algorithm | 0 | 3 | 0.0% | +| CWE-352 | Cross-Site Request Forgery | 0 | 1 | 0.0% | +| CWE-400 | Resource Exhaustion | 0 | 1 | 0.0% | +| CWE-404 | Improper Resource Shutdown | 0 | 2 | 0.0% | +| CWE-693 | Protection Mechanism Failure | 0 | 1 | 0.0% | +| CWE-798 | Hard-coded Credentials | 0 | 2 | 0.0% | +| CWE-942 | Overly Permissive CORS | 0 | 4 | 0.0% | + +*Last updated: 2025-06-17 15:45:17 UTC* + + + ## License This project is licensed under the terms of the MIT open source license. From 76dde6153439d03435cf04a3bd9810bbd457014a Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Tue, 17 Jun 2025 15:49:58 +0100 Subject: [PATCH 3/4] docs: Add coverage reporting section to CONTRIBUTING.md --- CONTRIBUTING.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef95efb..eb96fff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,6 +65,60 @@ To test the CodeQL libraries and queries, you can run the following command: ./scripts/run-tests.sh ``` +### Coverage Reporting + +The project includes a coverage script to track CodeQL query coverage and generate reports. The script analyzes all available queries and provides detailed coverage statistics. + +#### Running the Coverage Script + +To update coverage data and generate a report: + +```bash +python scripts/coverage.py +``` + +This will: +- Load the existing `.coverage.json` file +- Run `codeql resolve queries` to discover all available queries +- Update the coverage data with query metadata +- Generate a markdown coverage report +- Update the README.md file with the latest coverage statistics + +#### Coverage Script Options + +The coverage script supports several command-line options: + +```bash +# Generate only markdown output (useful for CI/CD) +python scripts/coverage.py --markdown-only + +# Update coverage data but skip README.md update +python scripts/coverage.py --no-readme-update + +# Show help and available options +python scripts/coverage.py --help +``` + +#### Coverage Report Features + +The generated coverage report includes: + +- **Coverage Badge**: Dynamically colored based on coverage percentage +- **Summary Statistics**: Total queries, covered queries, coverage percentage +- **Category Breakdown**: Coverage by query category (Security, Diagnostics) +- **CWE Analysis**: Coverage by Common Weakness Enumeration categories +- **Timestamp**: When the report was last generated + +#### Understanding Coverage Data + +The coverage script tracks: +- **Total Queries**: All queries discovered in `ql/src/` +- **Covered Queries**: Queries that have associated tests (initially 0, updated as tests are added) +- **Categories**: Query categories like `security` and `diagnostics` +- **CWE Categories**: Security queries mapped to Common Weakness Enumeration identifiers + +To mark a query as "covered", you'll need to create corresponding test files in the `ql/test/` directory structure. + ## Resources - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) From 986489eedfe34fb60da23cada5e33e31c70128de Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Tue, 17 Jun 2025 15:52:34 +0100 Subject: [PATCH 4/4] fix(docs): Update formatting --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb96fff..4690165 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,6 +78,7 @@ python scripts/coverage.py ``` This will: + - Load the existing `.coverage.json` file - Run `codeql resolve queries` to discover all available queries - Update the coverage data with query metadata @@ -112,6 +113,7 @@ The generated coverage report includes: #### Understanding Coverage Data The coverage script tracks: + - **Total Queries**: All queries discovered in `ql/src/` - **Covered Queries**: Queries that have associated tests (initially 0, updated as tests are added) - **Categories**: Query categories like `security` and `diagnostics`