diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..eb58acf --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,120 @@ +name : "CodeQL Analysis Workflow" + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + id-token: write + contents: read + actions: read + + +jobs: + codeql: + name: Analyse + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + language_details: + - name: javascript + queries_path: ./examples/codeql/queries/js + - name: go + queries_path: ./examples/codeql/queries/go + + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + examples/codeql/** + sparse-checkout-cone-mode: false + + - name: Set up CodeQL for ${{ matrix.language_details.name }} + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language_details.name }} + config-file: examples/codeql/codeql-config.yml + queries: ${{ matrix.language_details.queries_path }} + + - name: Setup Jfrog CLI for go + uses: jfrog/setup-jfrog-cli@v4 + env: + JF_URL: ${{ vars.ARTIFACTORY_URL }} + JF_ACCESS_TOKEN: ${{ secrets.ARTIFACTORY_ACCESS_TOKEN }} + + + - name: Setup Go + if: matrix.language_details.name == 'go' + uses: actions/setup-go@v5 + with: + go-version: '1.24.3' + + + - name: Run CodeQL Analysis for ${{ matrix.language_details.name }} + uses: github/codeql-action/analyze@v3 + with: + category: "security-and-quality" + output: results-${{ matrix.language_details.name }} + upload: false + + - name: Convert SARIF to Markdown + run: | + python ./examples/codeql/sarif_to_markdown.py \ + results-${{ matrix.language_details.name }}/${{ matrix.language_details.name }}.sarif \ + results-${{ matrix.language_details.name }}/${{ matrix.language_details.name }}-report.md + + - name: Build and Publish ${{ matrix.language_details.name }} package + env: + GO_CODE_PATH: examples/codeql/go + JS_CODE_PATH: examples/codeql/js + run: | + if [ ${{ matrix.language_details.name }} == 'go' ]; then + cd $GO_CODE_PATH + # Configure JFrog CLI for Go + jf go-config --repo-resolve=go-remote --repo-deploy=go-local \ + --server-id-deploy=setup-jfrog-cli-server \ + --server-id-resolve=setup-jfrog-cli-server + + jf gp --build-name=my-go-build --build-number=${{ github.run_number }} v0.0.${{ github.run_number }} + jf rt bp my-go-build ${{ github.run_number }} + elif [ ${{ matrix.language_details.name }} == 'javascript' ]; then + cd $JS_CODE_PATH + jf npm-config --repo-resolve=javascript-remote --repo-deploy=javascript-local \ + --server-id-deploy=setup-jfrog-cli-server \ + --server-id-resolve=setup-jfrog-cli-server + + jf npm publish --build-name=my-javascript-build --build-number=${{ github.run_number }} + jf rt bp my-javascript-build ${{ github.run_number }} + fi + cd - + continue-on-error: true + + - name: Attach Evidence Using JFrog CLI + run: | + jf config show + if [ ${{ matrix.language_details.name }} == 'go' ]; then + PACKAGE_VERSION="v0.0.${{ github.run_number }}" + jf evd create \ + --package-name "jfrog.com/mygobuild" \ + --package-version $PACKAGE_VERSION \ + --package-repo-name go-local \ + --key "${{ secrets.CODEQL_SIGNING_KEY }}" \ + --key-alias ${{ vars.CODEQL_KEY_ALIAS }} \ + --predicate "results-go/go.sarif" \ + --predicate-type "http://github.com/CodeQL/static-analysis" \ + --markdown "results-go/go-report.md" + elif [ ${{ matrix.language_details.name }} == 'javascript' ]; then + PACKAGE_VERSION="0.0.1" + jf evd create \ + --package-name my-javascript-build \ + --package-version $PACKAGE_VERSION \ + --package-repo-name javascript-local \ + --key "${{ secrets.CODEQL_SIGNING_KEY }}" \ + --key-alias ${{ vars.CODEQL_KEY_ALIAS }} \ + --predicate "results-javascript/javascript.sarif" \ + --predicate-type "http://github.com/CodeQL/static-analysis" \ + --markdown "results-javascript/javascript-report.md" + fi diff --git a/examples/codeql/README.md b/examples/codeql/README.md new file mode 100644 index 0000000..5e3d908 --- /dev/null +++ b/examples/codeql/README.md @@ -0,0 +1,108 @@ +# CodeQL Security Analysis Evidence Example + +This example demonstrates how to automate CodeQL security analysis for Go and JavaScript code, and attach the scan results as signed evidence to the packages in JFrog Artifactory using GitHub Actions and JFrog CLI. + +## Overview +The workflow performs CodeQL analysis on Go and JavaScript codebases, publishes the packages to Artifactory, and attaches the CodeQL analysis results as evidence. This enables traceability and security compliance in your CI/CD pipeline. + +## Prerequisites +- JFrog CLI 2.76.1 or above (installed automatically in the workflow) +- Go 1.24.3 (for Go analysis) +- Node.js 18.x (for JavaScript analysis) +- The following GitHub repository variables: + - `ARTIFACTORY_URL` (Artifactory base URL) +- The following GitHub repository secrets: + - `ARTIFACTORY_ACCESS_TOKEN` (Artifactory access token) + - `JFROG_SIGNING_KEY` + +## Supported Languages +- Go +- JavaScript + +## Workflow Steps +1. **Checkout Repository** + - Performs sparse checkout of required directories + - Only checks out the necessary CodeQL examples and queries + +2. **Setup CodeQL** + - Initializes CodeQL for the specified language + - Configures custom queries from `examples/codeql/queries/{language}` + +3. **Setup Build Environment** + - For Go: Installs Go 1.24.3 + - For JavaScript: Installs Node.js + - Configures JFrog CLI with Artifactory credentials + +4. **Run CodeQL Analysis** + - Performs CodeQL analysis for security and quality + - Generates SARIF format results + - Saves results without uploading to GitHub + +5. **Build and Publish Packages** + - For Go: + - Configures JFrog CLI for Go repository + - Publishes package to Artifactory Go repository + - For JavaScript: + - Configures JFrog CLI for npm repository + - Publishes package to Artifactory npm repository + +6. **Attach Evidence** + - Attaches CodeQL analysis results as signed evidence to the published packages + +## Environment Setup + +### Go Package Configuration +```yaml +jf go-config --repo-resolve=go-remote --repo-deploy=go-local \ + --server-id-deploy=setup-jfrog-cli-server \ + --server-id-resolve=setup-jfrog-cli-server +``` + +### JavaScript Package Configuration +```yaml +jf npm-config --repo-resolve=javascript-remote --repo-deploy=javascript-local \ + --server-id-deploy=setup-jfrog-cli-server \ + --server-id-resolve=setup-jfrog-cli-server +``` + +## Evidence Attachment +The workflow attaches CodeQL analysis results as evidence using the following format: + +### For Go Packages: +```yaml +jf evd create \ +--package-name "jfrog.com/mygobuild" \ +--package-version $PACKAGE_VERSION \ +--package-repo-name go-local \ +--key "${{ secrets.CODEQL_SIGNING_KEY }}" \ +--key-alias ${{ vars.CODEQL_KEY_ALIAS }} \ +--predicate "results-go/go.sarif" \ +--predicate-type "http://github.com/CodeQL/static-analysis" \ +--markdown "results-go/go-report.md" +``` + +### For JavaScript Packages: +```yaml +jf evd create \ +--package-name my-javascript-build \ +--package-version $PACKAGE_VERSION \ +--package-repo-name javascript-local \ +--key "${{ secrets.CODEQL_SIGNING_KEY }}" \ +--key-alias ${{ vars.CODEQL_KEY_ALIAS }} \ +--predicate "results-javascript/javascript.sarif" \ +--predicate-type "http://github.com/CodeQL/static-analysis" \ +--markdown "results-javascript/javascript-report.md" +``` + +## Workflow Trigger +The analysis is triggered on: +- Push to main branch +- Manual workflow dispatch + +## References +- [CodeQL Documentation](https://codeql.github.com/docs/) +- [JFrog CLI Documentation](https://www.jfrog.com/confluence/display/CLI/CLI+for+JFrog+Artifactory) +- [GitHub CodeQL Action](https://github.com/github/codeql-action) +- [JFrog Evidence Management](https://www.jfrog.com/confluence/display/JFROG/Evidence+Management) + + diff --git a/examples/codeql/codeql-config.yml b/examples/codeql/codeql-config.yml new file mode 100644 index 0000000..4ed78c1 --- /dev/null +++ b/examples/codeql/codeql-config.yml @@ -0,0 +1,16 @@ +name: "Package-Specific CodeQL Config" + +paths-ignore: + - '**/node_modules/**' + - '**/vendor/**' + - '**/dist/**' + - '**/build/**' + - '**/coverage/**' + - '**/test/**' + - '**/tests/**' + - '**/*.spec.js' + - '**/*.test.js' + - '**/*.spec.ts' + +paths: + - examples/codeql/ diff --git a/examples/codeql/go/go.mod b/examples/codeql/go/go.mod new file mode 100644 index 0000000..e54ac75 --- /dev/null +++ b/examples/codeql/go/go.mod @@ -0,0 +1,3 @@ +module jfrog.com/mygobuild + +go 1.24.3 diff --git a/examples/codeql/go/main.go b/examples/codeql/go/main.go new file mode 100644 index 0000000..e8558e5 --- /dev/null +++ b/examples/codeql/go/main.go @@ -0,0 +1,20 @@ +package mygobuild + +import ( + "fmt" + "time" +) + +// Greetter method with 5 params : name, place, age, fromDate, tillDate +func Greetter(name string, place string, age int, fromDate time.Time, tillDate time.Time) { + fmt.Printf("Welcome %s , Please verify your details:\n", name) + fmt.Printf("Place: %s\n", place) + fmt.Printf("Age: %d\n", age) + fmt.Printf("From Date: %s\n", fromDate.Format("2006-01-02")) + fmt.Printf("Till Date: %s\n", tillDate.Format("2006-01-02")) + fmt.Println("Thank you for providing your details!") +} + +func main() { + Greetter("John Doe", "New York", 30, time.Now().AddDate(0, 0, -7), time.Now()) +} \ No newline at end of file diff --git a/examples/codeql/js/index.js b/examples/codeql/js/index.js new file mode 100644 index 0000000..70dde6e --- /dev/null +++ b/examples/codeql/js/index.js @@ -0,0 +1,4 @@ +export function greet(name, place, age, from, till) { + console.log(`Hello ${name} from ${place}, you are ${age} years old!`); + console.log(`You are visiting from ${from} to ${till}.`); +} diff --git a/examples/codeql/js/package.json b/examples/codeql/js/package.json new file mode 100644 index 0000000..1818a13 --- /dev/null +++ b/examples/codeql/js/package.json @@ -0,0 +1,12 @@ +{ + "name": "my-javascript-build", + "version": "0.0.1", + "description": "Dummy package for testing CodeQL JavaScript queries", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "JFrog", + "license": "ISC" +} diff --git a/examples/codeql/queries/go/codeql-pack.lock.yml b/examples/codeql/queries/go/codeql-pack.lock.yml new file mode 100644 index 0000000..91e885f --- /dev/null +++ b/examples/codeql/queries/go/codeql-pack.lock.yml @@ -0,0 +1,24 @@ +--- +lockVersion: 1.0.0 +dependencies: + codeql/dataflow: + version: 2.0.8 + codeql/go-all: + version: 4.2.6 + codeql/go-queries: + version: 1.2.1 + codeql/mad: + version: 1.0.24 + codeql/ssa: + version: 2.0.0 + codeql/suite-helpers: + version: 1.0.24 + codeql/threat-models: + version: 1.0.24 + codeql/tutorial: + version: 1.0.24 + codeql/typetracking: + version: 2.0.8 + codeql/util: + version: 2.0.11 +compiled: false diff --git a/examples/codeql/queries/go/go-too-many-params.ql b/examples/codeql/queries/go/go-too-many-params.ql new file mode 100644 index 0000000..66f6534 --- /dev/null +++ b/examples/codeql/queries/go/go-too-many-params.ql @@ -0,0 +1,12 @@ +/** + * @name Functions with too many parameters + * @description Finds Go functions that have more than 3 parameters. + * @kind problem + * @problem.severity warning + * @id go/too-many-parameters + */ +import go + +from Function f +where f.getNumParameter() > 3 +select f, "Function " + f.getName() + " has " + f.getNumParameter().toString() + " parameters, which is more than the allowed 3." diff --git a/examples/codeql/queries/go/qlpack.yml b/examples/codeql/queries/go/qlpack.yml new file mode 100644 index 0000000..a7e1e70 --- /dev/null +++ b/examples/codeql/queries/go/qlpack.yml @@ -0,0 +1,5 @@ +name: sample/go-queries +version: 0.0.1 +dependencies: + codeql/go-queries: "*" +extractor: go \ No newline at end of file diff --git a/examples/codeql/queries/js/codeql-pack.lock.yml b/examples/codeql/queries/js/codeql-pack.lock.yml new file mode 100644 index 0000000..d241f46 --- /dev/null +++ b/examples/codeql/queries/js/codeql-pack.lock.yml @@ -0,0 +1,32 @@ +--- +lockVersion: 1.0.0 +dependencies: + codeql/dataflow: + version: 2.0.8 + codeql/javascript-all: + version: 2.6.4 + codeql/javascript-queries: + version: 1.6.1 + codeql/mad: + version: 1.0.24 + codeql/regex: + version: 1.0.24 + codeql/ssa: + version: 2.0.0 + codeql/suite-helpers: + version: 1.0.24 + codeql/threat-models: + version: 1.0.24 + codeql/tutorial: + version: 1.0.24 + codeql/typetracking: + version: 2.0.8 + codeql/typos: + version: 1.0.24 + codeql/util: + version: 2.0.11 + codeql/xml: + version: 1.0.24 + codeql/yaml: + version: 1.0.24 +compiled: false diff --git a/examples/codeql/queries/js/js-too-many-params.ql b/examples/codeql/queries/js/js-too-many-params.ql new file mode 100644 index 0000000..df76f73 --- /dev/null +++ b/examples/codeql/queries/js/js-too-many-params.ql @@ -0,0 +1,14 @@ +/** + * @name Too many parameters + * @description Functions with too many parameters can be hard to read and maintain. + * @kind problem + * @precision high + * @problem.severity warning + * @id js/too-many-params + * @tags maintainability + */ +import javascript + +from Function f +where f.getNumParameter() > 3 +select f, "Function " + f.getName() + " has " + f.getNumParameter().toString() + " parameters, which is more than the allowed 3." \ No newline at end of file diff --git a/examples/codeql/queries/js/qlpack.yml b/examples/codeql/queries/js/qlpack.yml new file mode 100644 index 0000000..941d9f6 --- /dev/null +++ b/examples/codeql/queries/js/qlpack.yml @@ -0,0 +1,5 @@ +name: sample/js-queries +version: 0.0.1 +dependencies: + codeql/javascript-queries: "*" +extractor: javascript \ No newline at end of file diff --git a/examples/codeql/sarif_to_markdown.py b/examples/codeql/sarif_to_markdown.py new file mode 100644 index 0000000..405c9bf --- /dev/null +++ b/examples/codeql/sarif_to_markdown.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 + +""" +CodeQL SARIF to Markdown Converter + +This script converts CodeQL SARIF output files to readable Markdown format. +It includes severity ratings, CVSS scores, and detailed analysis information. +""" + +import json +import sys +import logging +from datetime import datetime +from typing import Dict, List, Optional, Any +import platform +import os + +class SeverityFormatter: + """Handles severity-related formatting and conversions.""" + + EMOJI_MAP = { + 'error': '🔴', + 'warning': '🟡', + 'note': '🔵', + 'none': '⚪' + } + + CVSS_RANGES = [ + (9.0, 'Critical'), + (7.0, 'High'), + (4.0, 'Medium'), + (0.0, 'Low') + ] + + @classmethod + def get_emoji(cls, level: str) -> str: + return cls.EMOJI_MAP.get(level.lower(), cls.EMOJI_MAP['none']) + + @classmethod + def get_cvss_rating(cls, security_severity: Any) -> str: + if not security_severity: + return "N/A" + try: + score = float(security_severity) + for threshold, rating in cls.CVSS_RANGES: + if score >= threshold: + return f"{rating} ({score})" + return f"Low ({score})" + except (ValueError, TypeError): + return str(security_severity) + +class MarkdownBuilder: + def __init__(self, sarif_data: Dict): + self.data = sarif_data + self.formatter = SeverityFormatter() + self.sections: List[str] = [] + + def add_header(self) -> None: + codeql_version = "unknown" + if self.data.get('runs'): + tool_info = self.data['runs'][0].get('tool', {}).get('driver', {}) + codeql_version = tool_info.get('version', 'unknown') + + self.sections.extend([ + "# 🔍 CodeQL Security Analysis Report", + "\n## Scan Details", + f"**Scan Type**: CodeQL Static Analysis\n", + f"**Scan Date**: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}\n", + f"**Operating System**: {platform.system()} {platform.release()}\n", + f"**Analysis Tool**: CodeQL", + "\n---\n" + ]) + + def add_tool_info(self) -> None: + if not self.data.get('runs'): + return + tool = self.data['runs'][0].get('tool', {}).get('driver', {}) + self.sections.extend([ + "\n## 🛠️ Analysis Details", + f"- **Tool**: {tool.get('name', 'CodeQL')}", + f"- **Version**: {tool.get('semanticVersion', tool.get('version', 'N/A'))}", + ]) + + # Map artifact index to language + artifact_lang = {} + for notification in tool.get('notifications', []): + lang = notification.get('properties', {}).get('languageDisplayName') + locations = notification.get('locations', []) + for loc in locations: + idx = loc.get('physicalLocation', {}).get('artifactLocation', {}).get('index') + if lang and idx is not None: + artifact_lang[idx] = lang + + + + def add_summary(self) -> None: + severity_count = { + 'error': 0, + 'warning': 0, + 'note': 0, + 'none': 0 + } + total_issues = 0 + + for run in self.data.get('runs', []): + # Collect rules from driver and all extensions + rules = {rule['id']: rule for rule in run.get('tool', {}).get('driver', {}).get('rules', [])} + for ext in run.get('tool', {}).get('extensions', []): + for rule in ext.get('rules', []): + rules[rule['id']] = rule + + for result in run.get('results', []): + rule_id = result.get('ruleId', 'unknown') + rule = rules.get(rule_id, {}) + rule_severity = rule.get('properties', {}).get('problem.severity', 'none') + level = result.get('level', rule_severity).lower() + if level not in severity_count: + severity_count[level] = 0 + severity_count[level] += 1 + total_issues += 1 + + self.sections.extend([ + "\n## 📊 Analysis Summary", + f"\n**Total Issues Found**: {total_issues}", + "\n### Severity Breakdown" + ]) + + for severity in ['error', 'warning', 'note', 'none']: + count = severity_count.get(severity, 0) + emoji = self.formatter.get_emoji(severity) + self.sections.append(f"- {emoji} **{severity.title()}**: {count}") + + + def add_query_info(self) -> None: + self.sections.append("\n## 📝 Query Information") + + unique_queries = set() + for run in self.data.get('runs', []): + for rule in run.get('tool', {}).get('driver', {}).get('rules', []): + if rule['id'] not in unique_queries: + unique_queries.add(rule['id']) + properties = rule.get('properties', {}) + + self.sections.extend([ + f"\n### {rule.get('name', rule['id'])}", + f"- **ID**: `{rule['id']}`" + ]) + + if 'security-severity' in properties: + cvss = self.formatter.get_cvss_rating(properties['security-severity']) + self.sections.append(f"- **CVSS Score**: {cvss}") + + severity = properties.get('problem.severity', 'none') + emoji = self.formatter.get_emoji(severity) + self.sections.append(f"- **Severity**: {emoji} {severity.title()}") + + if 'tags' in properties: + tags = ', '.join(f'`{tag}`' for tag in properties['tags']) + self.sections.append(f"- **Tags**: {tags}") + + description = rule.get('description', {}).get('text', 'No description available') + self.sections.extend(['', description, '']) + + def add_findings(self) -> None: + self.sections.extend([ + "\n## 🔍 Detailed Findings", + "\n| Severity | Query | Location | Description |", + "|----------|--------|-----------|-------------|" + ]) + + for run in self.data.get('runs', []): + # Collect rules from driver and all extensions + rules = {rule['id']: rule for rule in run.get('tool', {}).get('driver', {}).get('rules', [])} + for ext in run.get('tool', {}).get('extensions', []): + for rule in ext.get('rules', []): + rules[rule['id']] = rule + + for result in run.get('results', []): + rule_id = result.get('ruleId', 'unknown') + rule = rules.get(rule_id, {}) + rule_name = rule.get('name', rule_id) + + # Fallback to rule severity if result.level is missing + rule_severity = rule.get('properties', {}).get('problem.severity', 'none') + severity = result.get('level', rule_severity) + + emoji = self.formatter.get_emoji(severity) + location = self._format_location(result.get('locations', [])) + message = result.get('message', {}).get('text', 'No description available') + + self.sections.append( + f"| {emoji} {severity.title()} | {rule_name} | {location} | {message} |" + ) + def _format_location(self, locations: List[Dict]) -> str: + if not locations: + return "N/A" + loc = locations[0].get('physicalLocation', {}) + file_path = loc.get('artifactLocation', {}).get('uri', 'unknown') + region = loc.get('region', {}) + start_line = region.get('startLine', '?') + end_line = region.get('endLine', start_line) + location = f"`{file_path}:{start_line}`" + if start_line != end_line: + location += f"-`{end_line}`" + return location + + + def build(self) -> str: + self.add_header() + self.add_tool_info() + self.add_summary() + self.add_query_info() + self.add_findings() + return '\n'.join(self.sections) + +def setup_logging(): + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + +def main(): + setup_logging() + logger = logging.getLogger(__name__) + + if len(sys.argv) != 3: + logger.error("Incorrect number of arguments") + print("Usage: python sarif_to_markdown.py ") + sys.exit(1) + + input_file = sys.argv[1] + output_file = sys.argv[2] + + try: + logger.info(f"Reading SARIF file: {input_file}") + with open(input_file, 'r') as f: + sarif_data = json.load(f) + + logger.info("Converting SARIF to Markdown") + builder = MarkdownBuilder(sarif_data) + markdown_content = builder.build() + + logger.info(f"Writing Markdown file: {output_file}") + with open(output_file, 'w') as f: + f.write(markdown_content) + + logger.info("Conversion completed successfully") + + except FileNotFoundError: + logger.error(f"Input file not found: {input_file}") + sys.exit(1) + except json.JSONDecodeError: + logger.error(f"Invalid SARIF JSON in file: {input_file}") + sys.exit(1) + except Exception as e: + logger.error(f"Unexpected error: {str(e)}") + sys.exit(1) + +if __name__ == "__main__": + main()