Skip to content

DAST Security Scan

DAST Security Scan #68

Workflow file for this run

# 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