DAST Security Scan #68
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Dynamic Application Security Testing (DAST) with OWASP ZAP | |
| # Tests running application for security vulnerabilities | |
| name: DAST Security Scan | |
| on: | |
| # Run on schedule for staging/demo environment | |
| schedule: | |
| # Run daily at 4 AM UTC (after deployments) | |
| - cron: '0 4 * * *' | |
| # Manual trigger for testing | |
| workflow_dispatch: | |
| inputs: | |
| target_url: | |
| description: 'Target URL to scan' | |
| required: false | |
| default: 'https://cmz-chatbot-demo.netlify.app' | |
| scan_type: | |
| description: 'Scan type' | |
| required: false | |
| default: 'baseline' | |
| type: choice | |
| options: | |
| - baseline | |
| - full | |
| - api | |
| jobs: | |
| zap-scan: | |
| name: OWASP ZAP DAST Scan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set target URL | |
| id: target | |
| env: | |
| TARGET_URL: ${{ github.event.inputs.target_url }} | |
| run: | | |
| if [ -n "$TARGET_URL" ]; then | |
| echo "url=$TARGET_URL" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "url=https://cmz-chatbot-demo.netlify.app" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Create ZAP configuration | |
| run: | | |
| mkdir -p .zap | |
| # Create ZAP rules file to customize scanning | |
| cat > .zap/rules.tsv << 'EOF' | |
| # ZAP Scanning Rules Configuration | |
| # Format: RULE_ID THRESHOLD [COMMENT] | |
| # Ignore common false positives | |
| 10038 OFF # Content Security Policy Header Not Set (handled by Netlify) | |
| 10017 OFF # Cross-Domain JavaScript Source File Inclusion (React app bundles) | |
| 10096 OFF # Timestamp Disclosure (build timestamps are not sensitive) | |
| # Enable important security checks | |
| 40012 LOW # Cross Site Scripting (Reflected) | |
| 40014 LOW # Cross Site Scripting (Persistent) | |
| 40016 LOW # Cross Site Scripting (Persistent) - Prime | |
| 40017 LOW # Cross Site Scripting (Persistent) - Spider | |
| 40018 LOW # SQL Injection | |
| 90020 LOW # Remote OS Command Injection | |
| EOF | |
| # Create ZAP context configuration | |
| cat > .zap/context.xml << 'EOF' | |
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | |
| <configuration> | |
| <contexts> | |
| <context> | |
| <name>CMZ Chatbot Context</name> | |
| <desc>Security context for CMZ chatbot application</desc> | |
| <inscope>true</inscope> | |
| <incregexes>https://cmz-chatbot-demo\.netlify\.app.*</incregexes> | |
| <excregexes>.*logout.*</excregexes> | |
| <tech> | |
| <include>Db</include> | |
| <include>JavaScript</include> | |
| <include>React</include> | |
| </tech> | |
| </context> | |
| </contexts> | |
| </configuration> | |
| EOF | |
| - name: Run ZAP Baseline Scan | |
| if: github.event.inputs.scan_type != 'full' && github.event.inputs.scan_type != 'api' | |
| uses: zaproxy/action-baseline@v0.12.0 | |
| with: | |
| target: ${{ steps.target.outputs.url }} | |
| rules_file_name: '.zap/rules.tsv' | |
| cmd_options: '-a -d -T 60 -m 10' | |
| fail_action: false | |
| artifact_name: 'zap-baseline-report' | |
| - name: Run ZAP Full Scan | |
| if: github.event.inputs.scan_type == 'full' | |
| uses: zaproxy/action-full-scan@v0.10.0 | |
| with: | |
| target: ${{ steps.target.outputs.url }} | |
| rules_file_name: '.zap/rules.tsv' | |
| cmd_options: '-a -d -T 120' | |
| fail_action: false | |
| artifact_name: 'zap-full-report' | |
| - name: Run ZAP API Scan | |
| if: github.event.inputs.scan_type == 'api' | |
| env: | |
| TARGET_URL: ${{ steps.target.outputs.url }} | |
| run: | | |
| # Check if OpenAPI spec is accessible | |
| if curl -f -s "$TARGET_URL/openapi.json" > /dev/null 2>&1; then | |
| docker run --rm -v "$(pwd)":/zap/wrk/:rw \ | |
| -t ghcr.io/zaproxy/zaproxy:stable \ | |
| zap-api-scan.py \ | |
| -t "$TARGET_URL/openapi.json" \ | |
| -f openapi \ | |
| -r zap-api-report.html \ | |
| -w zap-api-report.md \ | |
| -J zap-api-report.json | |
| else | |
| echo "No OpenAPI spec found, skipping API scan" | |
| fi | |
| - name: Create security summary | |
| if: always() | |
| env: | |
| TARGET_URL: ${{ steps.target.outputs.url }} | |
| SCAN_TYPE: ${{ github.event.inputs.scan_type || 'baseline' }} | |
| run: | | |
| { | |
| echo "## 🔍 Dynamic Application Security Testing (DAST)" | |
| echo "" | |
| echo "**Target**: $TARGET_URL" | |
| echo "**Scan Type**: $SCAN_TYPE" | |
| echo "**Date**: $(date -u)" | |
| echo "" | |
| } > dast-summary.md | |
| # Check if ZAP report exists and parse it | |
| if [ -f "report_html.html" ]; then | |
| # Extract basic info from HTML report (simplified parsing) | |
| if grep -q "High" report_html.html; then | |
| HIGH_COUNT=$(grep -o "High" report_html.html | wc -l || echo "0") | |
| echo "- 🚨 **High Risk**: $HIGH_COUNT issues" >> dast-summary.md | |
| fi | |
| if grep -q "Medium" report_html.html; then | |
| MEDIUM_COUNT=$(grep -o "Medium" report_html.html | wc -l || echo "0") | |
| echo "- ⚠️ **Medium Risk**: $MEDIUM_COUNT issues" >> dast-summary.md | |
| fi | |
| if grep -q "Low" report_html.html; then | |
| LOW_COUNT=$(grep -o "Low" report_html.html | wc -l || echo "0") | |
| echo "- 📝 **Low Risk**: $LOW_COUNT issues" >> dast-summary.md | |
| fi | |
| fi | |
| - name: Upload DAST results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: dast-scan-results-${{ github.run_number }} | |
| path: | | |
| report_html.html | |
| report_md.md | |
| report_json.json | |
| zap-api-report.* | |
| dast-summary.md | |
| retention-days: 30 | |
| - name: Create security issue for high-risk findings | |
| if: always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| // Check if high-risk findings exist | |
| let hasHighRisk = false; | |
| if (fs.existsSync('report_html.html')) { | |
| const report = fs.readFileSync('report_html.html', 'utf8'); | |
| if (report.includes('High') || report.includes('Critical')) { | |
| hasHighRisk = true; | |
| } | |
| } | |
| if (hasHighRisk) { | |
| const summary = fs.existsSync('dast-summary.md') | |
| ? fs.readFileSync('dast-summary.md', 'utf8') | |
| : 'High-risk security findings detected in DAST scan.'; | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: '🚨 High-Risk Security Findings - DAST Scan', | |
| body: `${summary}\n\n**Scan Run**: ${context.runNumber}\n**Workflow**: ${context.workflow}`, | |
| labels: ['security', 'high-risk', 'dast-findings'] | |
| }); | |
| } | |
| accessibility-scan: | |
| name: Accessibility Security Scan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Run Pa11y accessibility scan | |
| continue-on-error: true | |
| run: | | |
| npm install -g pa11y pa11y-reporter-html | |
| # Scan main pages for accessibility issues (some are security-related) | |
| pa11y https://cmz-chatbot-demo.netlify.app \ | |
| --reporter html > pa11y-report.html || true | |
| - name: Upload accessibility results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: accessibility-scan-results | |
| path: pa11y-report.html | |
| retention-days: 15 |