Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cspell/general-technical.txt
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,7 @@ playment
playsinline
Plotly
pluggable
pluginid
pluralsight
pmks
pngs
Expand Down Expand Up @@ -1109,6 +1110,7 @@ rfcs
rgba
rhn
risc
riskcode
rlhf
rmem
roadmap
Expand Down Expand Up @@ -1510,6 +1512,7 @@ yymmddhhmmss
yymmddhhmss
zabbix
Zachman
zaproxy
Zeebe
Zeek
zerega
Expand Down
1 change: 1 addition & 0 deletions .cspell/industry-acronyms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ CMMS
COMMITMSG
C-SCRM
CUDA
DAST
FAISS
FIDAP
FMEA
Expand Down
105 changes: 105 additions & 0 deletions .github/workflows/dast-zap-scan.yml
Original file line number Diff line number Diff line change
@@ -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" <<EOF
## DAST ZAP Baseline Scan Results

| Metric | Value |
|--------|-------|
| Target | \`http://localhost:5173/\` |
| Status | Completed |
| Security Tab | [View alerts](/${{ github.repository }}/security/code-scanning?query=tool%3Azap) |
| Artifacts | [View run artifacts](/${{ github.repository }}/actions/runs/${{ github.run_id }}) |

EOF
40 changes: 40 additions & 0 deletions .github/workflows/dependabot-security-prefix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Dependabot Security Prefix

on:
pull_request:
types:
- opened

permissions:
contents: read
pull-requests: write

jobs:
retitle-security-pr:
name: Retitle Security PRs
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
permissions:
contents: read
pull-requests: write

steps:
- name: Fetch Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"

- name: Retitle PR with security prefix
if: steps.metadata.outputs.ghsa-id != '' || steps.metadata.outputs.cvss != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
new_title="${PR_TITLE/chore(deps)/security(deps)}"
new_title="${new_title/chore(deps-dev)/security(deps-dev)}"
if [[ "${new_title}" != "${PR_TITLE}" ]]; then
gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --title "${new_title}"
echo "Retitled PR #${PR_NUMBER}: ${new_title}"
fi
13 changes: 13 additions & 0 deletions .zap/rules.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
10003 WARN Vulnerable JS Library
10010 IGNORE Cookie No HttpOnly Flag
10011 IGNORE Cookie Without Secure Flag
10015 IGNORE Re-examine Cache-control Directives
10017 IGNORE Cross-Domain JavaScript Source File Inclusion
10020 WARN Anti-clickjacking Header Missing
10021 WARN X-Content-Type-Options Header Missing
10035 WARN Strict-Transport-Security Header Not Set
10036 IGNORE Server Leaks Version Information via "Server" HTTP Response Header Field
10038 WARN Content Security Policy (CSP) Header Not Set
10049 IGNORE Storable and Cacheable Content
10055 WARN CSP: Wildcard Directive
10109 IGNORE Modern Web Application (informational)
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ Contributions are welcome across infrastructure code, deployment automation, doc

If you are new to the project, start with issues labeled `good first issue` or documentation updates before making larger changes.

## Contributor License Agreement

Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit <https://cla.opensource.microsoft.com>.

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
Expand Down
94 changes: 94 additions & 0 deletions scripts/security/zap-to-sarif.py
Original file line number Diff line number Diff line change
@@ -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]} <zap-report.json> <output.sarif>", 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()
Loading