diff --git a/.cspell/general-technical.txt b/.cspell/general-technical.txt index 0d95127d..2c673771 100644 --- a/.cspell/general-technical.txt +++ b/.cspell/general-technical.txt @@ -945,6 +945,7 @@ playment playsinline Plotly pluggable +pluginid pluralsight pmks pngs @@ -1109,6 +1110,7 @@ rfcs rgba rhn risc +riskcode rlhf rmem roadmap @@ -1510,6 +1512,7 @@ yymmddhhmmss yymmddhhmss zabbix Zachman +zaproxy Zeebe Zeek zerega diff --git a/.cspell/industry-acronyms.txt b/.cspell/industry-acronyms.txt index 5c520492..10d4adbe 100644 --- a/.cspell/industry-acronyms.txt +++ b/.cspell/industry-acronyms.txt @@ -12,6 +12,7 @@ CMMS COMMITMSG C-SCRM CUDA +DAST FAISS FIDAP FMEA diff --git a/.github/workflows/dast-zap-scan.yml b/.github/workflows/dast-zap-scan.yml new file mode 100644 index 00000000..1c84af79 --- /dev/null +++ b/.github/workflows/dast-zap-scan.yml @@ -0,0 +1,105 @@ +name: DAST ZAP Baseline Scan + +on: + schedule: + # Weekly scan: Sundays at 05:00 UTC (after Scorecard 03:00, Gitleaks 04:00) + - cron: "0 5 * * 0" + workflow_dispatch: + +permissions: + contents: read + +jobs: + scan: + name: ZAP Baseline Scan + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + issues: write + concurrency: + group: dast-zap-scan + cancel-in-progress: false + defaults: + run: + shell: pwsh + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Build and start dataviewer + shell: bash + working-directory: src/dataviewer + env: + HMI_LOCAL_DATA_PATH: ./data/test-dataset + run: docker compose up -d --build --wait + + - name: Verify containers are healthy + run: | + $response = Invoke-WebRequest -Uri "http://localhost:8000/health" -UseBasicParsing + if ($response.StatusCode -ne 200) { throw "Backend health check failed" } + $response = Invoke-WebRequest -Uri "http://localhost:5173/" -UseBasicParsing + if ($response.StatusCode -ne 200) { throw "Frontend health check failed" } + + - name: ZAP baseline scan + uses: zaproxy/action-baseline@de8ad967d3548d44ef623df22cf95c3b0baf8b25 # v0.15.0 + with: + target: "http://localhost:5173/" + rules_file_name: ".zap/rules.tsv" + allow_issue_writing: true + issue_title: "DAST ZAP Baseline Scan Report" + fail_action: "" + artifact_name: "zap-baseline-report" + + - name: Convert ZAP report to SARIF + if: always() + shell: bash + run: | + set -euo pipefail + mkdir -p results + python3 scripts/security/zap-to-sarif.py report_json.json results/zap-results.sarif + + - name: Upload SARIF to Security tab + if: always() + uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 + continue-on-error: true + with: + sarif_file: results/zap-results.sarif + category: dast-zap-baseline + + - name: Upload scan results + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: dast-zap-results + path: | + report_json.json + report_md.md + report_html.html + results/zap-results.sarif + retention-days: 90 + + - name: Stop dataviewer containers + if: always() + shell: bash + working-directory: src/dataviewer + run: docker compose down + + - name: Add job summary + if: always() + shell: bash + run: | + cat >> "$GITHUB_STEP_SUMMARY" <. + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Follow the instructions provided by the bot. You only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any questions or comments. + ## Getting Started 1. Read the [Contributing Guide](docs/contributing/README.md) for prerequisites, workflow, and conventions diff --git a/scripts/security/zap-to-sarif.py b/scripts/security/zap-to-sarif.py new file mode 100755 index 00000000..835b0ee7 --- /dev/null +++ b/scripts/security/zap-to-sarif.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: MIT +"""Convert ZAP JSON report to SARIF 2.1.0 format for GitHub Code Scanning.""" + +import json +import sys +from pathlib import Path + +SARIF_SCHEMA = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json" + +LEVEL_MAP = { + "0": "none", # Informational + "1": "note", # Low + "2": "warning", # Medium + "3": "error", # High +} + + +def convert(zap_json: dict) -> dict: + rules = [] + results = [] + rule_ids_seen = set() + + for site in zap_json.get("site", []): + for alert in site.get("alerts", []): + rule_id = f"ZAP-{alert['pluginid']}" + if rule_id not in rule_ids_seen: + rule_ids_seen.add(rule_id) + rules.append( + { + "id": rule_id, + "name": alert.get("name", ""), + "shortDescription": {"text": alert.get("name", "")}, + "fullDescription": {"text": alert.get("desc", "").strip()}, + "helpUri": alert.get("reference", ""), + "properties": {"tags": ["security", "DAST"]}, + } + ) + + for instance in alert.get("instances", []): + results.append( + { + "ruleId": rule_id, + "level": LEVEL_MAP.get(str(alert.get("riskcode", "0")), "warning"), + "message": {"text": alert.get("solution", alert.get("name", ""))}, + "locations": [ + { + "physicalLocation": { + "artifactLocation": {"uri": instance.get("uri", "")}, + } + } + ], + } + ) + + return { + "$schema": SARIF_SCHEMA, + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "OWASP ZAP", + "informationUri": "https://www.zaproxy.org/", + "rules": rules, + } + }, + "results": results, + } + ], + } + + +def main(): + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(2) + + input_path = Path(sys.argv[1]) + output_path = Path(sys.argv[2]) + + try: + zap_data = json.loads(input_path.read_text()) + except (FileNotFoundError, json.JSONDecodeError) as exc: + print(f"Error reading ZAP report '{input_path}': {exc}", file=sys.stderr) + sys.exit(1) + + sarif_data = convert(zap_data) + output_path.write_text(json.dumps(sarif_data, indent=2)) + + +if __name__ == "__main__": + main()