diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..d8b0552 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,184 @@ +name: Integration Tests + +on: + push: + branches: + - main + - "claude/**" + pull_request: + branches: + - main + +jobs: + # ── Unit tests: pure logic, no external dependencies ─────────────────────── + unit-tests: + name: Unit Tests (Python) + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + + - name: Install test dependencies + run: pip install -r requirements-test.txt + + - name: Run unit tests + run: pytest tests/unit/ -v --tb=short --junit-xml=unit-test-results.xml + + - name: Upload unit test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: unit-test-results + path: unit-test-results.xml + + # ── Integration tests: scripts invoked end-to-end (no API creds needed) ─── + python-integration-tests: + name: Python Script Integration Tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + + - name: Install test dependencies + run: pip install -r requirements-test.txt + + - name: Run Python integration tests + run: | + pytest tests/integration/ \ + --ignore=tests/integration/test_api_connectivity.py \ + --ignore=tests/integration/test_shell_scripts.py \ + -v --tb=short \ + --junit-xml=integration-test-results.xml + + - name: Upload integration test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: integration-test-results + path: integration-test-results.xml + + # ── Shell script integration tests (bash syntax + shellcheck) ───────────── + shell-integration-tests: + name: Shell Script Tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install shellcheck + run: sudo apt-get update -q && sudo apt-get install -y shellcheck + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install test dependencies + run: pip install -r requirements-test.txt + + - name: Run shell script tests + run: | + pytest tests/integration/test_shell_scripts.py \ + -v --tb=short \ + --junit-xml=shell-test-results.xml + + - name: Upload shell test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: shell-test-results + path: shell-test-results.xml + + # ── Combined coverage report ─────────────────────────────────────────────── + coverage: + name: Test Coverage + runs-on: ubuntu-latest + needs: [unit-tests, python-integration-tests] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: pip install -r requirements-test.txt + + - name: Run tests with coverage + run: | + pytest tests/ \ + --ignore=tests/integration/test_api_connectivity.py \ + --cov=Scripts \ + --cov-report=xml \ + --cov-report=term-missing \ + -q + continue-on-error: true + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage.xml + + # ── Live API connectivity tests (main branch + secrets only) ─────────────── + api-connectivity-tests: + name: Veracode API Connectivity + runs-on: ubuntu-latest + if: > + github.event_name == 'push' && + github.ref == 'refs/heads/main' && + secrets.VERACODE_API_ID != '' + environment: veracode-integration + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + pip install -r requirements-test.txt + pip install veracode-api-signing 2>/dev/null || true + + - name: Configure Veracode credentials + run: | + mkdir -p ~/.veracode + printf '[default]\nveracode_api_key_id = %s\nveracode_api_key_secret = %s\n' \ + "$VERACODE_API_ID" "$VERACODE_API_KEY" > ~/.veracode/credentials + env: + VERACODE_API_ID: ${{ secrets.VERACODE_API_ID }} + VERACODE_API_KEY: ${{ secrets.VERACODE_API_KEY }} + + - name: Run API connectivity tests + env: + VERACODE_API_ID: ${{ secrets.VERACODE_API_ID }} + VERACODE_API_KEY: ${{ secrets.VERACODE_API_KEY }} + run: | + pytest tests/integration/test_api_connectivity.py \ + -m api -v --tb=short \ + --junit-xml=api-test-results.xml + continue-on-error: true + + - name: Upload API test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: api-test-results + path: api-test-results.xml diff --git a/.github/workflows/qat.yml b/.github/workflows/qat.yml new file mode 100644 index 0000000..f1c28e0 --- /dev/null +++ b/.github/workflows/qat.yml @@ -0,0 +1,263 @@ +name: QAT (Quality Assurance Testing) + +on: + push: + branches: + - main + - "claude/**" + pull_request: + branches: + - main + +jobs: + # ── Python linting (flake8) ──────────────────────────────────────────────── + python-lint: + name: Python Lint (flake8) + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install flake8 + run: pip install flake8 + + - name: Run flake8 on Release scripts + run: | + flake8 Scripts/Release/ \ + --max-line-length=120 \ + --extend-ignore=E501,W503,E302,E303 \ + --statistics \ + --count \ + --format=default + continue-on-error: true + + - name: Run flake8 on Dev Python scripts + run: | + flake8 Scripts/Dev/ \ + --max-line-length=120 \ + --extend-ignore=E501,W503,E302,E303,F401,F811 \ + --exclude=Scripts/Dev/Reference,Scripts/Dev/archive \ + --statistics \ + --count + continue-on-error: true + + - name: Run flake8 on tests + run: | + flake8 tests/ \ + --max-line-length=120 \ + --extend-ignore=E501,W503 \ + --statistics \ + --count + + # ── Shell script linting (ShellCheck) ───────────────────────────────────── + shell-lint: + name: Shell Lint (ShellCheck) + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install ShellCheck + run: sudo apt-get update -q && sudo apt-get install -y shellcheck + + - name: Lint Release shell scripts + run: | + echo "=== Linting Release scripts ===" + find Scripts/Release/ -name "*.sh" -print0 | \ + xargs -0 shellcheck --severity=warning --format=tty + continue-on-error: true + + - name: Lint Dev bash scripts + run: | + echo "=== Linting Dev bash scripts ===" + find Scripts/Dev/bash_scripts/ -name "*.sh" -print0 | \ + xargs -0 shellcheck --severity=warning --format=tty + continue-on-error: true + + - name: Lint XML API scripts + run: | + echo "=== Linting XML API scripts ===" + find xml_api_calls/ -name "*.sh" -print0 | \ + xargs -0 shellcheck --severity=warning --format=tty + continue-on-error: true + + # ── JSON validation ──────────────────────────────────────────────────────── + json-validation: + name: JSON File Validation + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate all JSON files + run: | + FAILED=0 + while IFS= read -r -d '' file; do + if python3 -c "import json; json.load(open('$file'))" 2>/dev/null; then + echo "PASS: $file" + else + echo "FAIL: $file" + python3 -c "import json; json.load(open('$file'))" 2>&1 || true + FAILED=1 + fi + done < <(find . \ + -name "*.json" \ + -not -path "./.git/*" \ + -not -path "./Scripts/Dev/Test/results.json" \ + -not -path "./Scripts/Dev/Test/filtered_results.json" \ + -not -path "./Scripts/results.json" \ + -not -path "./Scripts/Dev/Reference/*" \ + -print0) + + if [ "$FAILED" -eq 1 ]; then + echo "One or more JSON files failed validation" + exit 1 + fi + echo "All JSON files passed validation" + + # ── YAML validation ──────────────────────────────────────────────────────── + yaml-validation: + name: YAML Validation + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install yamllint + run: pip install yamllint + + - name: Validate GitHub workflow YAML files + run: | + yamllint .github/workflows/ \ + -d "{extends: relaxed, rules: {line-length: {max: 120}}}" + + # ── Full test suite execution ────────────────────────────────────────────── + full-test-suite: + name: Full Test Suite + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install ShellCheck (for shell tests) + run: sudo apt-get update -q && sudo apt-get install -y shellcheck + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + + - name: Install test dependencies + run: pip install -r requirements-test.txt + + - name: Run full test suite (excluding API tests) + run: | + pytest tests/ \ + --ignore=tests/integration/test_api_connectivity.py \ + -v \ + --tb=long \ + --junit-xml=qat-test-results.xml \ + -q + + - name: Upload QAT test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: qat-test-results + path: qat-test-results.xml + + - name: Publish test results summary + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: qat-test-results.xml + check_name: "QAT Test Results" + continue-on-error: true + + # ── PowerShell script analysis ───────────────────────────────────────────── + powershell-analysis: + name: PowerShell Script Analysis + runs-on: windows-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install PSScriptAnalyzer + shell: pwsh + run: Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser + + - name: Analyze PowerShell scripts + shell: pwsh + run: | + $scripts = Get-ChildItem -Path "Scripts" -Filter "*.ps1" -Recurse + $hasErrors = $false + foreach ($script in $scripts) { + Write-Host "Analyzing: $($script.FullName)" + $results = Invoke-ScriptAnalyzer -Path $script.FullName -Severity Warning,Error + if ($results) { + $results | Format-Table -AutoSize + $hasErrors = $true + } else { + Write-Host "PASS: $($script.Name)" + } + } + if ($hasErrors) { + Write-Warning "PSScriptAnalyzer found issues. Review output above." + } + continue-on-error: true + + # ── Script structure and metadata checks ────────────────────────────────── + structure-checks: + name: Script Structure Checks + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check Python scripts have shebangs or module docstrings + run: | + echo "Checking Python script headers..." + MISSING=0 + for f in Scripts/Release/*.py; do + if ! head -5 "$f" | grep -qE '^#!|^#|^"""'; then + echo "WARNING: $f may be missing a shebang or docstring header" + MISSING=$((MISSING + 1)) + fi + done + echo "Files checked. $MISSING potential header issues found." + continue-on-error: true + + - name: Check shell scripts have shebangs + run: | + echo "Checking shell script shebangs..." + MISSING=0 + for f in Scripts/Release/*.sh; do + if ! head -1 "$f" | grep -q '^#!'; then + echo "WARNING: $f is missing a shebang line" + MISSING=$((MISSING + 1)) + fi + done + echo "Files checked. $MISSING shebangs missing." + continue-on-error: true + + - name: Verify test fixtures exist and are non-empty + run: | + echo "Verifying test fixtures..." + for f in tests/fixtures/allowlist.csv tests/fixtures/blacklist.csv tests/fixtures/glblacklist.csv; do + if [ ! -s "$f" ]; then + echo "FAIL: $f is missing or empty" + exit 1 + fi + echo "PASS: $f" + done diff --git a/.github/workflows/reusable-pipeline-scan.yml b/.github/workflows/reusable-pipeline-scan.yml new file mode 100644 index 0000000..ce5774e --- /dev/null +++ b/.github/workflows/reusable-pipeline-scan.yml @@ -0,0 +1,150 @@ +# ────────────────────────────────────────────────────────────────────────────── +# REUSABLE WORKFLOW: Veracode Pipeline Scan (callable from any repo) +# ────────────────────────────────────────────────────────────────────────────── +# Call this from another repo's workflow: +# +# jobs: +# security: +# uses: bnreplah/veracode-scripts/.github/workflows/reusable-pipeline-scan.yml@main +# secrets: inherit +# with: +# artifact_path: "target/app.jar" +# app_name: "My Application" +# fail_build: false +# +# Required secrets (passed via `secrets: inherit` or explicit mapping): +# VERACODE_API_ID +# VERACODE_API_KEY +# ────────────────────────────────────────────────────────────────────────────── + +name: Reusable — Veracode Pipeline Scan + +on: + workflow_call: + inputs: + # Path to the compiled/packaged artifact to scan + artifact_path: + description: "Path to the artifact file to scan (jar, war, zip, dll, exe)" + required: true + type: string + + # Veracode application name (used for labeling results) + app_name: + description: "Application name for scan labeling" + required: false + type: string + default: "${{ github.repository }}" + + # Whether to fail the calling workflow on findings above severity threshold + fail_build: + description: "Fail the workflow if findings exceed severity threshold" + required: false + type: boolean + default: false + + # Severity threshold when fail_build is true + fail_on_severity: + description: "Comma-separated severities that trigger a build failure" + required: false + type: string + default: "Very High, High" + + # Optional: baseline results file artifact name (from a previous run) + baseline_artifact: + description: "Name of a GitHub artifact containing a baseline results.json" + required: false + type: string + default: "" + + # Optional: Veracode policy to evaluate against + request_policy: + description: "Veracode policy name to evaluate results against" + required: false + type: string + default: "" + + # Runner OS — override if your artifact needs Windows (e.g. .NET) + runner: + description: "GitHub-hosted runner image" + required: false + type: string + default: "ubuntu-latest" + + secrets: + VERACODE_API_ID: + description: "Veracode API Key ID" + required: true + VERACODE_API_KEY: + description: "Veracode API Key Secret" + required: true + + outputs: + results_artifact: + description: "Name of the artifact containing pipeline scan results" + value: ${{ jobs.pipeline-scan.outputs.results_artifact }} + scan_status: + description: "Pipeline scan job result (success/failure/skipped)" + value: ${{ jobs.pipeline-scan.result }} + +jobs: + pipeline-scan: + name: Veracode Pipeline Scan — ${{ inputs.app_name }} + runs-on: ${{ inputs.runner }} + outputs: + results_artifact: pipeline-scan-results-${{ github.run_id }} + + steps: + - name: Checkout calling repository + uses: actions/checkout@v4 + + # Download artifact if uploaded by a preceding build job + - name: Download scan artifact + uses: actions/download-artifact@v4 + with: + name: scan-artifact + path: . + continue-on-error: true # OK if artifact doesn't exist (file may be in workspace) + + # Optionally download a baseline file for delta-only reporting + - name: Download baseline artifact + if: inputs.baseline_artifact != '' + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.baseline_artifact }} + path: . + continue-on-error: true + + - name: Veracode Pipeline Scan + id: scan + uses: veracode/pipeline-scan-action@v1 + with: + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + file: ${{ inputs.artifact_path }} + fail_build: ${{ inputs.fail_build }} + fail_on_severity: ${{ inputs.fail_on_severity }} + request_policy: ${{ inputs.request_policy }} + baseline_file: ${{ inputs.baseline_artifact != '' && 'results.json' || '' }} + + - name: Upload pipeline scan results + uses: actions/upload-artifact@v4 + if: always() + with: + name: pipeline-scan-results-${{ github.run_id }} + path: | + results.json + filtered_results.json + retention-days: 30 + + - name: Write step summary + if: always() + run: | + echo "## Veracode Pipeline Scan — ${{ inputs.app_name }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|---|---|" >> $GITHUB_STEP_SUMMARY + echo "| Artifact | \`${{ inputs.artifact_path }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Fail Build | ${{ inputs.fail_build }} |" >> $GITHUB_STEP_SUMMARY + echo "| Repository | ${{ github.repository }} |" >> $GITHUB_STEP_SUMMARY + echo "| Ref | ${{ github.ref_name }} |" >> $GITHUB_STEP_SUMMARY + echo "| Run | ${{ github.run_id }} |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/reusable-policy-scan.yml b/.github/workflows/reusable-policy-scan.yml new file mode 100644 index 0000000..cd0fd0c --- /dev/null +++ b/.github/workflows/reusable-policy-scan.yml @@ -0,0 +1,159 @@ +# ────────────────────────────────────────────────────────────────────────────── +# REUSABLE WORKFLOW: Veracode Policy Scan (callable from any repo) +# ────────────────────────────────────────────────────────────────────────────── +# Call this from another repo's workflow: +# +# jobs: +# security: +# uses: bnreplah/veracode-scripts/.github/workflows/reusable-policy-scan.yml@main +# secrets: inherit +# with: +# artifact_path: "target/app.jar" +# app_name: "My Application" +# sandbox_name: "" # leave blank for policy scan +# +# Required secrets (passed via `secrets: inherit` or explicit mapping): +# VERACODE_API_ID +# VERACODE_API_KEY +# ────────────────────────────────────────────────────────────────────────────── + +name: Reusable — Veracode Policy Scan + +on: + workflow_call: + inputs: + artifact_path: + description: "Path to the artifact to upload and scan" + required: true + type: string + + app_name: + description: "Veracode application profile name" + required: true + type: string + + # Leave blank to run a policy (main) scan; provide a name to scan in sandbox + sandbox_name: + description: "Sandbox name (blank = policy scan, non-blank = sandbox scan)" + required: false + type: string + default: "" + + # Build version label shown in the Veracode platform + build_version: + description: "Build/version label in the Veracode platform" + required: false + type: string + default: "${{ github.ref_name }}-${{ github.run_number }}" + + # Create sandbox if it doesn't exist (only relevant when sandbox_name set) + create_sandbox: + description: "Create the sandbox if it doesn't already exist" + required: false + type: boolean + default: true + + # When true, block the calling workflow until scan results are ready + wait_for_scan: + description: "Wait for scan to complete before returning" + required: false + type: boolean + default: false + + # When true, fail the workflow if the policy is not passed + fail_build: + description: "Fail the workflow when the policy result is not passed" + required: false + type: boolean + default: false + + # Behaviour when an incomplete scan exists: 0=cancel, 1=delete, 2=ignore + delete_incomplete: + description: "How to handle an existing incomplete scan (0/1/2)" + required: false + type: string + default: "1" + + runner: + description: "GitHub-hosted runner image" + required: false + type: string + default: "ubuntu-latest" + + secrets: + VERACODE_API_ID: + required: true + VERACODE_API_KEY: + required: true + + outputs: + scan_status: + description: "Policy scan job result (success/failure/skipped)" + value: ${{ jobs.policy-scan.result }} + scan_type: + description: "Whether this ran as a sandbox or policy scan" + value: ${{ jobs.policy-scan.outputs.scan_type }} + +jobs: + policy-scan: + name: > + Veracode ${{ inputs.sandbox_name != '' && 'Sandbox' || 'Policy' }} Scan + — ${{ inputs.app_name }} + runs-on: ${{ inputs.runner }} + outputs: + scan_type: ${{ steps.scan-type.outputs.type }} + + steps: + - name: Checkout calling repository + uses: actions/checkout@v4 + + - name: Download scan artifact + uses: actions/download-artifact@v4 + with: + name: scan-artifact + path: . + continue-on-error: true + + - name: Determine scan type + id: scan-type + run: | + if [ -n "${{ inputs.sandbox_name }}" ]; then + echo "type=sandbox" >> $GITHUB_OUTPUT + echo "Running SANDBOX scan in: ${{ inputs.sandbox_name }}" + else + echo "type=policy" >> $GITHUB_OUTPUT + echo "Running POLICY scan for: ${{ inputs.app_name }}" + fi + + - name: Veracode Upload and Scan + uses: veracode/veracode-uploadandscan-action@master + with: + appname: ${{ inputs.app_name }} + createprofile: true + filepath: ${{ inputs.artifact_path }} + version: ${{ inputs.build_version }} + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + sandboxname: ${{ inputs.sandbox_name }} + createsandbox: ${{ inputs.create_sandbox }} + deleteincompletescan: ${{ inputs.delete_incomplete }} + waitForScan: ${{ inputs.wait_for_scan }} + failbuild: ${{ inputs.fail_build }} + + - name: Write step summary + if: always() + run: | + SCAN_TYPE="${{ steps.scan-type.outputs.type }}" + echo "## Veracode ${SCAN_TYPE^} Scan — ${{ inputs.app_name }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|---|---|" >> $GITHUB_STEP_SUMMARY + echo "| Scan Type | ${SCAN_TYPE} |" >> $GITHUB_STEP_SUMMARY + echo "| Application | ${{ inputs.app_name }} |" >> $GITHUB_STEP_SUMMARY + if [ -n "${{ inputs.sandbox_name }}" ]; then + echo "| Sandbox | ${{ inputs.sandbox_name }} |" >> $GITHUB_STEP_SUMMARY + fi + echo "| Version | ${{ inputs.build_version }} |" >> $GITHUB_STEP_SUMMARY + echo "| Wait For Results | ${{ inputs.wait_for_scan }} |" >> $GITHUB_STEP_SUMMARY + echo "| Fail Build | ${{ inputs.fail_build }} |" >> $GITHUB_STEP_SUMMARY + echo "| Repository | ${{ github.repository }} |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml new file mode 100644 index 0000000..3ee742e --- /dev/null +++ b/.github/workflows/security-scan.yml @@ -0,0 +1,207 @@ +name: Security Scan + +on: + push: + branches: + - main + - "claude/**" + pull_request: + branches: + - main + schedule: + # Run weekly on Monday at 08:00 UTC + - cron: "0 8 * * 1" + +permissions: + contents: read + security-events: write + actions: read + +jobs: + # ── Python static security analysis (Bandit) ────────────────────────────── + python-bandit: + name: Python Security Analysis (Bandit) + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Bandit + run: pip install bandit[toml] + + - name: Run Bandit (high + medium severity) + run: | + bandit -r Scripts/ \ + -f json -o bandit-report.json \ + -ll \ + --exclude Scripts/Dev/Reference,Scripts/src/bin \ + || true + + - name: Print Bandit summary + run: | + bandit -r Scripts/ \ + -f txt \ + -ll \ + --exclude Scripts/Dev/Reference,Scripts/src/bin \ + || true + + - name: Upload Bandit report + uses: actions/upload-artifact@v4 + if: always() + with: + name: bandit-security-report + path: bandit-report.json + + # ── Shell script security analysis (ShellCheck) ─────────────────────────── + shell-shellcheck: + name: Shell Security Analysis (ShellCheck) + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run ShellCheck on Release scripts + uses: ludeeus/action-shellcheck@master + with: + scandir: "./Scripts/Release" + severity: warning + format: tty + continue-on-error: true + + - name: Run ShellCheck on Dev bash scripts + uses: ludeeus/action-shellcheck@master + with: + scandir: "./Scripts/Dev/bash_scripts" + severity: warning + format: tty + continue-on-error: true + + - name: Run ShellCheck on XML API scripts + uses: ludeeus/action-shellcheck@master + with: + scandir: "./xml_api_calls" + severity: warning + format: tty + continue-on-error: true + + # ── Secret scanning (Gitleaks) ──────────────────────────────────────────── + secret-scanning: + name: Secret Scanning (Gitleaks) + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run Gitleaks + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true + + # ── Python dependency audit (pip-audit) ─────────────────────────────────── + dependency-audit: + name: Dependency Vulnerability Audit (pip-audit) + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install pip-audit + run: pip install pip-audit + + - name: Audit test dependencies + run: | + pip install -r requirements-test.txt + pip-audit --desc on 2>/dev/null || true + + - name: Audit all Python files for unsafe imports + run: | + echo "Scanning Python scripts for known vulnerable patterns..." + grep -rn "import pickle\|eval(\|exec(\|subprocess.call.*shell=True" \ + Scripts/ --include="*.py" || echo "No flagged patterns found." + + # ── Semgrep SAST ────────────────────────────────────────────────────────── + semgrep: + name: Semgrep SAST + runs-on: ubuntu-latest + if: github.actor != 'dependabot[bot]' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python for Semgrep + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Semgrep + run: pip install semgrep + + - name: Run Semgrep on Python scripts + run: | + semgrep --config p/python \ + --config p/security-audit \ + Scripts/ \ + --json -o semgrep-report.json \ + || true + semgrep --config p/python \ + --config p/security-audit \ + Scripts/ \ + --text \ + || true + + - name: Upload Semgrep report + uses: actions/upload-artifact@v4 + if: always() + with: + name: semgrep-report + path: semgrep-report.json + + # ── Credentials file check ───────────────────────────────────────────────── + credentials-check: + name: Credentials File Check + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check for committed credential files + run: | + echo "Checking for credential files that should not be committed..." + FOUND=0 + + # Check for .veracode/credentials files (except in test fixtures) + if find . -path "./.git" -prune -o \ + -name "credentials" -path "*/.veracode/*" \ + -not -path "*/Dev/Test/*" -print | grep -q .; then + echo "WARNING: .veracode/credentials file found outside of test fixtures" + find . -path "./.git" -prune -o \ + -name "credentials" -path "*/.veracode/*" \ + -not -path "*/Dev/Test/*" -print + FOUND=1 + fi + + # Check for files with hardcoded API key patterns + if grep -rn "veracode_api_key_id\s*=\s*[a-z0-9]\{8\}" \ + --include="*.sh" --include="*.py" --include="*.ps1" \ + Scripts/ 2>/dev/null; then + echo "WARNING: Possible hardcoded API key ID found" + FOUND=1 + fi + + if [ "$FOUND" -eq 0 ]; then + echo "No credential issues detected." + fi + continue-on-error: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d54dac --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +*.egg-info/ +dist/ +build/ +.eggs/ + +# Test outputs +.pytest_cache/ +.coverage +coverage.xml +htmlcov/ +*-test-results.xml +bandit-report.json +semgrep-report.json + +# Virtual environments +.venv/ +venv/ +env/ + +# IDE +.vscode/settings.json +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Credentials (never commit) +.veracode/credentials + +# Generated scan outputs +input.json +pipeline-scan-LATEST.zip +pipeline-scan.jar diff --git a/README.md b/README.md index 4b9e8ba..5f1d441 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,205 @@ -# Veracode-scripts # +# Veracode Security Testing SDK Framework -## General ## -This script is to be used as a interactive installer and agent for making differn't types of calls to Veracode's APIs and an interactive installer for setting up and configuring your Veracode SAST Products and SCA products. +[![Integration Tests](https://github.com/bnreplah/veracode-scripts/actions/workflows/integration-tests.yml/badge.svg)](https://github.com/bnreplah/veracode-scripts/actions/workflows/integration-tests.yml) +[![Security Scan](https://github.com/bnreplah/veracode-scripts/actions/workflows/security-scan.yml/badge.svg)](https://github.com/bnreplah/veracode-scripts/actions/workflows/security-scan.yml) +[![QAT](https://github.com/bnreplah/veracode-scripts/actions/workflows/qat.yml/badge.svg)](https://github.com/bnreplah/veracode-scripts/actions/workflows/qat.yml) +--- -## Includes ## -- Veracode REST and XML API calls -- Veracode REST conversion to XML API output -- SRM (Scriptable Request Modification) API Specification Configuration https://docs.veracode.com/r/Example_Script_for_Scriptable_Request_Modification_Authentication?tocId=GxBzVtHR5GnF~kPAmh0MNw +## Overview -## Use ## +A comprehensive SDK and tooling framework for Veracode application security testing. +It wraps the Veracode REST and XML APIs with helper scripts, automated analysis utilities, and an installation framework — targeting SAST, DAST, SCA, container security, and misconfiguration detection from a single, unified toolset. +### Goals +- **Correlate findings** across DAST, SAST, SCA, container findings, and security misconfigurations +- **Directed analysis** — surface overlapping data paths, common flaw sources in static analysis, and cross-scan vulnerability patterns +- **Automated recommendations** — link findings to security training modules and remediation guidance +- **Cross-platform installation** — thin installer in Bash (`.sh`), PowerShell (`.ps1`), and Go (`.exe`) to bootstrap the full toolset +--- -## Commands ## +## Workflows & Badges +| Workflow | Description | Badge | +|---|---|---| +| **Integration Tests** | Unit tests + Python script integration tests + shell script syntax/shellcheck | [![Integration Tests](https://github.com/bnreplah/veracode-scripts/actions/workflows/integration-tests.yml/badge.svg)](https://github.com/bnreplah/veracode-scripts/actions/workflows/integration-tests.yml) | +| **Security Scan** | Bandit (Python), ShellCheck, Gitleaks secret scanning, pip-audit, Semgrep SAST | [![Security Scan](https://github.com/bnreplah/veracode-scripts/actions/workflows/security-scan.yml/badge.svg)](https://github.com/bnreplah/veracode-scripts/actions/workflows/security-scan.yml) | +| **QAT** | flake8 linting, ShellCheck lint, JSON/YAML validation, PSScriptAnalyzer, full test suite | [![QAT](https://github.com/bnreplah/veracode-scripts/actions/workflows/qat.yml/badge.svg)](https://github.com/bnreplah/veracode-scripts/actions/workflows/qat.yml) | +--- +## What's Included +### Scripts/Release — Production-Ready +| Script | Type | Description | +|---|---|---| +| `DASTWebAppRequest-std.py` | Python | Format and submit Dynamic Web App scan requests from CLI or piped JSON | +| `BlackList-std.py` | Python | Build DAST blocklist/scan settings from CSV files | +| `DAST-ls-v2.sh` | Bash | List DAST scans and results with pagination and verbose reporting | +| `DAST-rescan.sh` | Bash | Trigger rescans for Dynamic Analysis | +| `SearchBuildByName.sh` | Bash | Search Veracode application builds by name across apps | +| `vdb-purl-lte.sh` | Bash | Veracode vulnerability DB PURL lookup (lite) | +| `veracode-installer.sh` | Bash | Install and configure Veracode CLI tooling | -A working repository of custom script integrations for veracode +### Scripts/Dev — In Development + +| Directory | Contents | +|---|---| +| `DASTFramework/` | Modular DAST configuration framework (request builder, status polling, hooks, API scan) | +| `DBlookup/` | CPE/PURL vulnerability database lookup utilities | +| `bash_scripts/` | SCA library search, pipeline scan, sandbox promotion, upload scripts | +| `ps_scripts/` | PowerShell scripts: Java API wrapper management, scan status monitoring | +| `Test/` | Test scripts, sample data, and debugging utilities | + +### xml_api_calls — Legacy XML API + +Sequential workflow scripts for the Veracode XML API: +`0_getapplist` → `1_getapplist` → `2_getbuildlist` → `3_getsandboxlist` → `4_detailedreport` + +--- + +## Veracode APIs Used + +| API | Purpose | +|---|---| +| **Dynamic Analysis REST API** | DAST scan creation, configuration, scheduling, status | +| **Upload/Results API (XML)** | SAST scan submission, build management, detailed reports | +| **SCA REST API** | Workspace/project scanning, library/dependency findings | +| **Identity API** | Team and user management (replacing deprecated XML Admin API) | +| **Pipeline Scan API** | CI/CD integrated scanning with pre-scan file size checks | +| **Veracode CLI** | Modern CLI wrapper for SAST, SCA, and SBOM generation | + +--- + +## Authentication + +Credentials can be supplied via: + +1. **Credentials file** — `~/.veracode/credentials`: + ```ini + [default] + veracode_api_key_id = YOUR_API_ID + veracode_api_key_secret = YOUR_API_KEY + ``` + +2. **Environment variables**: + ```bash + export VERACODE_API_ID="your-api-id" + export VERACODE_API_KEY="your-api-key" + ``` + +3. **SCA Agent token** (for `srcclr`): + ```bash + export SRCCLR_API_TOKEN="your-token" + ``` + +--- + +## Quick Start + +### Install Veracode Tooling +```bash +# Install Veracode CLI +bash Scripts/Release/veracode-installer.sh --force-install-vccli + +# Install SCA CLI agent +bash Scripts/Release/veracode-installer.sh --install-sca-cli + +# Install Java API Wrapper +bash Scripts/Release/veracode-installer.sh --install-java-api-wrapper + +# Install Pipeline Scanner +bash Scripts/Release/veracode-installer.sh --install-pipeline-scanner +``` + +### Create a DAST Analysis Request +```bash +# Interactive mode +python Scripts/Release/DASTWebAppRequest-std.py + +# Non-interactive / pipe mode (stdout JSON for use with http or curl) +python Scripts/Release/DASTWebAppRequest-std.py \ + "My-App-Scan" \ + "https://target.example.com/" \ + "owner@company.com" \ + "Security Team" \ + | http POST "https://api.veracode.com/was/configservice/v1/analyses" \ + --auth-type=veracode_hmac +``` + +### List DAST Scans +```bash +bash Scripts/Release/DAST-ls-v2.sh +``` + +### SCA Library Search +```bash +bash Scripts/Dev/bash_scripts/SCA-Library-ProjectSearch.sh "log4j" +``` + +--- + +## Running Tests + +```bash +# Install test dependencies +pip install -r requirements-test.txt + +# Run all unit tests (no credentials needed) +pytest tests/unit/ -v + +# Run all integration tests (no credentials needed) +pytest tests/integration/ --ignore=tests/integration/test_api_connectivity.py -v + +# Run full suite +pytest tests/ --ignore=tests/integration/test_api_connectivity.py -v + +# Run with coverage +pytest tests/ --ignore=tests/integration/test_api_connectivity.py \ + --cov=Scripts --cov-report=term-missing + +# Run live API connectivity tests (requires credentials) +pytest tests/integration/test_api_connectivity.py -m api -v +``` + +### Test Structure + +``` +tests/ +├── conftest.py # Shared fixtures (tmp_work_dir, paths) +├── fixtures/ +│ ├── allowlist.csv # DAST allowlist test fixture +│ ├── blacklist.csv # DAST blocklist test fixture +│ └── glblacklist.csv # Global blocklist test fixture +├── unit/ +│ ├── test_email_validation.py # Email regex validation logic +│ ├── test_schedule_helpers.py # Scan schedule helper functions +│ └── test_csv_parsing.py # CSV → JSON parsing logic +└── integration/ + ├── test_dast_web_request_script.py # DASTWebAppRequest-std.py end-to-end + ├── test_blacklist_script.py # BlackList-std.py end-to-end + ├── test_shell_scripts.py # Bash syntax + shellcheck for all .sh + └── test_api_connectivity.py # Live Veracode API calls (needs creds) +``` + +### GitHub Secrets Required (for API tests) + +| Secret | Description | +|---|---| +| `VERACODE_API_ID` | Veracode API Key ID | +| `VERACODE_API_KEY` | Veracode API Key Secret | + +--- + +## SRM (Scriptable Request Modification) + +Supports Veracode SRM API specification configuration for authenticated dynamic scans. +Reference: [Veracode SRM Documentation](https://docs.veracode.com/r/Example_Script_for_Scriptable_Request_Modification_Authentication?tocId=GxBzVtHR5GnF~kPAmh0MNw) + +--- + +## License + +See [LICENSE](LICENSE) for details. diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..97294a5 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,11 @@ +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = -v --tb=short +markers = + unit: Pure unit tests with no external dependencies + integration: Integration tests invoking scripts end-to-end + api: Tests requiring live Veracode API credentials + slow: Tests that may take significant time to run diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..a090932 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,3 @@ +# Test dependencies for the Veracode SDK framework +pytest>=7.4.0 +pytest-cov>=4.1.0 diff --git a/templates/workflows/README.md b/templates/workflows/README.md new file mode 100644 index 0000000..ea30aed --- /dev/null +++ b/templates/workflows/README.md @@ -0,0 +1,66 @@ +# Veracode CI/CD Workflow Templates + +Copy any template from this directory into your repo's `.github/workflows/` folder, then follow the `# CUSTOMIZE:` and `# TODO:` markers to tailor it. + +## Templates + +### By Scan Type + +| File | Scan Type | When to Use | +|---|---|---| +| [`pipeline-scan.yml`](pipeline-scan.yml) | SAST (fast, inline) | Every PR and commit — fast feedback, no policy gate | +| [`policy-scan-sast.yml`](policy-scan-sast.yml) | SAST (full policy) | Main/release branches — authoritative policy result | +| [`sandbox-scan-promote.yml`](sandbox-scan-promote.yml) | SAST Sandbox | Feature branches — scan in sandbox, promote to policy on merge | +| [`sca-agent-scan.yml`](sca-agent-scan.yml) | SCA (dependencies) | Every PR — catch vulnerable OSS libraries | +| [`dast-web-scan.yml`](dast-web-scan.yml) | DAST (dynamic) | Scheduled or manual — test running web application | +| [`container-scan.yml`](container-scan.yml) | Container / IaC | On image build — scan Docker image layers | +| [`all-scans-devops.yml`](all-scans-devops.yml) | All scans | Full DevOps pipeline with build → all security gates | + +### By Language (Build + Scan) + +| File | Language | Package Manager | +|---|---|---| +| [`by-language/java-maven.yml`](by-language/java-maven.yml) | Java | Maven | +| [`by-language/java-gradle.yml`](by-language/java-gradle.yml) | Java | Gradle | +| [`by-language/nodejs.yml`](by-language/nodejs.yml) | JavaScript / TypeScript | npm / yarn | +| [`by-language/python.yml`](by-language/python.yml) | Python | pip | +| [`by-language/dotnet.yml`](by-language/dotnet.yml) | C# / .NET | dotnet CLI | +| [`by-language/go.yml`](by-language/go.yml) | Go | go build | + +### Reusable / Callable + +The two reusable workflows live in `.github/workflows/` so any repo can call them: + +```yaml +jobs: + security: + uses: bnreplah/veracode-scripts/.github/workflows/reusable-pipeline-scan.yml@main + secrets: inherit + with: + artifact_path: "target/app.jar" + app_name: "My Application" +``` + +See `.github/workflows/reusable-pipeline-scan.yml` and `.github/workflows/reusable-policy-scan.yml`. + +--- + +## Required GitHub Secrets + +Set these in your repo under **Settings → Secrets and variables → Actions**: + +| Secret | Required By | Description | +|---|---|---| +| `VERACODE_API_ID` | All scan types | Veracode API Key ID | +| `VERACODE_API_KEY` | All scan types | Veracode API Key Secret | +| `SRCCLR_API_TOKEN` | SCA scans | SourceClear / SCA agent token | + +--- + +## Quick Onboarding Checklist + +1. Copy the template(s) matching your stack to `.github/workflows/` +2. Add `VERACODE_API_ID`, `VERACODE_API_KEY` (and `SRCCLR_API_TOKEN` for SCA) to repo secrets +3. Search for `# CUSTOMIZE:` in the template and update every instance +4. Search for `# TODO:` and complete every action item +5. Push to trigger your first scan diff --git a/templates/workflows/all-scans-devops.yml b/templates/workflows/all-scans-devops.yml new file mode 100644 index 0000000..81c24f1 --- /dev/null +++ b/templates/workflows/all-scans-devops.yml @@ -0,0 +1,180 @@ +# ────────────────────────────────────────────────────────────────────────────── +# TEMPLATE: Veracode Full DevSecOps Pipeline — All Scans +# Combines: Pipeline Scan (PR) + SCA (PR) + Policy SAST (main) + Container (main) +# +# JOB FLOW: +# PR: build → pipeline-scan (SAST fast) + sca-scan (parallel) +# main: build → pipeline-scan + sca-scan → policy-scan → container-scan +# +# RUNTIME: PR ≈ 5–10 min | main ≈ 30 min – several hours +# ────────────────────────────────────────────────────────────────────────────── +# REQUIRED SECRETS: +# VERACODE_API_ID - Veracode API Key ID +# VERACODE_API_KEY - Veracode API Key Secret +# SRCCLR_API_TOKEN - SCA agent token +# ────────────────────────────────────────────────────────────────────────────── + +name: Veracode DevSecOps Pipeline + +on: + push: + branches: + - main + - master + - "release/**" + pull_request: + types: [opened, synchronize, reopened] + +env: + # CUSTOMIZE: Veracode application profile name + VERACODE_APP_NAME: "YOUR_APP_NAME" + # CUSTOMIZE: path to your compiled/packaged artifact + ARTIFACT_PATH: "app.zip" + BUILD_VERSION: "${{ github.ref_name }}-${{ github.run_number }}" + # CUSTOMIZE: Docker image name (for container scan on main) + IMAGE_NAME: "your-app-image" + +jobs: + # ── 1. BUILD ──────────────────────────────────────────────────────────────── + build: + name: Build + runs-on: ubuntu-latest + outputs: + artifact_name: ${{ steps.artifact.outputs.name }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # TODO: Replace with your actual build steps + # See by-language/ templates for language-specific examples + - name: TODO - Add build steps + run: echo "Replace with your build command" + + - name: Upload artifact + id: artifact + uses: actions/upload-artifact@v4 + with: + name: scan-artifact-${{ github.run_id }} + path: ${{ env.ARTIFACT_PATH }} + retention-days: 1 + + # ── 2. PIPELINE SCAN (every branch / PR) ─────────────────────────────────── + pipeline-scan: + name: SAST Pipeline Scan + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: scan-artifact-${{ github.run_id }} + + - name: Veracode Pipeline Scan + uses: veracode/pipeline-scan-action@v1 + with: + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + file: ${{ env.ARTIFACT_PATH }} + fail_build: false # CUSTOMIZE: true to gate PRs on findings + # fail_on_severity: "Very High, High" + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: pipeline-scan-results + path: results.json + + # ── 3. SCA SCAN (every branch / PR, parallel with pipeline scan) ─────────── + sca-scan: + name: SCA Dependency Scan + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + + # TODO: Add language-specific dependency install steps here + + - name: SCA Agent Scan + env: + SRCCLR_API_TOKEN: ${{ secrets.SRCCLR_API_TOKEN }} + run: | + curl -sSL https://download.sourceclear.com/ci.sh | sh -s scan \ + --allow-dirty \ + --recursive + continue-on-error: true + + # ── 4. POLICY SCAN (main branch only, after fast scans pass) ─────────────── + policy-scan: + name: SAST Policy Scan + runs-on: ubuntu-latest + needs: [pipeline-scan, sca-scan] + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: scan-artifact-${{ github.run_id }} + + - name: Veracode Policy Scan + uses: veracode/veracode-uploadandscan-action@master + with: + appname: ${{ env.VERACODE_APP_NAME }} + createprofile: true + filepath: ${{ env.ARTIFACT_PATH }} + version: ${{ env.BUILD_VERSION }} + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + deleteincompletescan: "1" + waitForScan: false # CUSTOMIZE: true to block on results + + # ── 5. CONTAINER SCAN (main branch only, if Dockerfile exists) ───────────── + container-scan: + name: Container Scan + runs-on: ubuntu-latest + needs: [pipeline-scan, sca-scan] + if: > + (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') && + hashFiles('Dockerfile') != '' + + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t ${{ env.IMAGE_NAME }}:${{ github.sha }} . + + - name: Install Veracode CLI + run: curl -fsS https://tools.veracode.com/veracode-cli/install | sh + + - name: Veracode container scan + env: + VERACODE_API_KEY_ID: ${{ secrets.VERACODE_API_ID }} + VERACODE_API_KEY_SECRET: ${{ secrets.VERACODE_API_KEY }} + run: | + ./veracode scan \ + --type image \ + --source ${{ env.IMAGE_NAME }}:${{ github.sha }} \ + --format table + continue-on-error: true + + # ── 6. SUMMARY ────────────────────────────────────────────────────────────── + security-summary: + name: Security Summary + runs-on: ubuntu-latest + needs: [pipeline-scan, sca-scan] + if: always() + + steps: + - name: Write job summary + run: | + echo "## Veracode DevSecOps Security Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Scan | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| SAST Pipeline Scan | ${{ needs.pipeline-scan.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| SCA Dependency Scan | ${{ needs.sca-scan.result }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Branch: \`${{ github.ref_name }}\` | Commit: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY diff --git a/templates/workflows/by-language/dotnet.yml b/templates/workflows/by-language/dotnet.yml new file mode 100644 index 0000000..8901ac6 --- /dev/null +++ b/templates/workflows/by-language/dotnet.yml @@ -0,0 +1,134 @@ +# ────────────────────────────────────────────────────────────────────────────── +# TEMPLATE: .NET (C#) + Veracode +# BUILD: dotnet publish → zip binaries + PDB files +# SCANS: Pipeline Scan (PR) + Policy Scan (main) + SCA +# +# PACKAGING NOTE: +# Veracode requires compiled DLLs AND PDB files (for source mapping). +# Include both in the zip. See: https://docs.veracode.com/r/compilation_net +# ────────────────────────────────────────────────────────────────────────────── +# REQUIRED SECRETS: VERACODE_API_ID, VERACODE_API_KEY, SRCCLR_API_TOKEN +# CUSTOMIZE: VERACODE_APP_NAME, DOTNET_VERSION, project path, configuration +# ────────────────────────────────────────────────────────────────────────────── + +name: .NET + Veracode + +on: + push: + branches: [main, master, "release/**"] + pull_request: + types: [opened, synchronize, reopened] + +env: + VERACODE_APP_NAME: "YOUR_APP_NAME" # CUSTOMIZE + DOTNET_VERSION: "8.0.x" # CUSTOMIZE: 6.0.x, 7.0.x, 8.0.x + BUILD_CONFIGURATION: Release + # CUSTOMIZE: path to your .csproj / .sln file + PROJECT_PATH: "." + BUILD_VERSION: "${{ github.ref_name }}-${{ github.run_number }}" + +jobs: + build: + name: .NET Build & Package + runs-on: ubuntu-latest # CUSTOMIZE: windows-latest if Windows-specific dependencies + + steps: + - uses: actions/checkout@v4 + + - name: Set up .NET ${{ env.DOTNET_VERSION }} + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore dependencies + run: dotnet restore ${{ env.PROJECT_PATH }} + + - name: Build + run: | + dotnet build ${{ env.PROJECT_PATH }} \ + -c ${{ env.BUILD_CONFIGURATION }} \ + --no-restore + + - name: Publish + run: | + dotnet publish ${{ env.PROJECT_PATH }} \ + -c ${{ env.BUILD_CONFIGURATION }} \ + --no-build \ + -o publish/ + # CUSTOMIZE: add --self-contained, -r , /p:PublishSingleFile=true + + # Package compiled DLLs + PDBs (both required by Veracode) + - name: Package binaries for Veracode + run: | + zip -r app.zip publish/ \ + -i "*.dll" "*.pdb" "*.exe" "*.json" "*.config" + echo "Artifact size: $(du -sh app.zip)" + + - uses: actions/upload-artifact@v4 + with: + name: dotnet-artifact + path: app.zip + retention-days: 1 + + pipeline-scan: + name: Veracode Pipeline Scan + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: { name: dotnet-artifact } + + - uses: veracode/pipeline-scan-action@v1 + with: + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + file: app.zip + fail_build: false + + - uses: actions/upload-artifact@v4 + if: always() + with: { name: pipeline-scan-results, path: results.json } + + policy-scan: + name: Veracode Policy Scan + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: { name: dotnet-artifact } + + - uses: veracode/veracode-uploadandscan-action@master + with: + appname: ${{ env.VERACODE_APP_NAME }} + createprofile: true + filepath: app.zip + version: ${{ env.BUILD_VERSION }} + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + deleteincompletescan: "1" + waitForScan: false + + sca-scan: + name: Veracode SCA (NuGet) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore (for SCA) + run: dotnet restore ${{ env.PROJECT_PATH }} + + - name: SCA Agent Scan + env: + SRCCLR_API_TOKEN: ${{ secrets.SRCCLR_API_TOKEN }} + run: curl -sSL https://download.sourceclear.com/ci.sh | sh -s scan --recursive + continue-on-error: true diff --git a/templates/workflows/by-language/go.yml b/templates/workflows/by-language/go.yml new file mode 100644 index 0000000..e19b31d --- /dev/null +++ b/templates/workflows/by-language/go.yml @@ -0,0 +1,137 @@ +# ────────────────────────────────────────────────────────────────────────────── +# TEMPLATE: Go + Veracode +# BUILD: go build → compiled binary, zipped with source +# SCANS: Pipeline Scan (PR) + Policy Scan (main) + SCA +# +# PACKAGING NOTE: +# Veracode can scan Go binaries OR zipped Go source. +# Compiled binary scan: faster, no source visibility. +# Source zip: recommended for better flaw attribution. +# See: https://docs.veracode.com/r/compilation_go +# ────────────────────────────────────────────────────────────────────────────── +# REQUIRED SECRETS: VERACODE_API_ID, VERACODE_API_KEY, SRCCLR_API_TOKEN +# CUSTOMIZE: VERACODE_APP_NAME, GO_VERSION, binary name +# ────────────────────────────────────────────────────────────────────────────── + +name: Go + Veracode + +on: + push: + branches: [main, master, "release/**"] + pull_request: + types: [opened, synchronize, reopened] + +env: + VERACODE_APP_NAME: "YOUR_APP_NAME" # CUSTOMIZE + GO_VERSION: "1.22" # CUSTOMIZE: 1.21, 1.22, 1.23 + BINARY_NAME: "app" # CUSTOMIZE: your binary output name + BUILD_VERSION: "${{ github.ref_name }}-${{ github.run_number }}" + +jobs: + build: + name: Go Build & Package + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Download dependencies + run: go mod download + + - name: Build binary + run: | + go build \ + -v \ + -o ${{ env.BINARY_NAME }} \ + ./... + # CUSTOMIZE: ./cmd/myapp/ or specific package path + + # Option A: Scan compiled binary (faster) + - name: Package binary for scan + run: | + zip app.zip ${{ env.BINARY_NAME }} + echo "Binary artifact: $(du -sh app.zip)" + + # Option B: Scan source (better flaw attribution) — uncomment to use instead + # - name: Package source for scan + # run: | + # zip -r app.zip . \ + # -x "*.git*" \ + # -x "vendor/*" \ + # -x "*_test.go" \ + # -x ".github/*" + + - uses: actions/upload-artifact@v4 + with: + name: go-artifact + path: app.zip + retention-days: 1 + + pipeline-scan: + name: Veracode Pipeline Scan + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: { name: go-artifact } + + - uses: veracode/pipeline-scan-action@v1 + with: + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + file: app.zip + fail_build: false + + - uses: actions/upload-artifact@v4 + if: always() + with: { name: pipeline-scan-results, path: results.json } + + policy-scan: + name: Veracode Policy Scan + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: { name: go-artifact } + + - uses: veracode/veracode-uploadandscan-action@master + with: + appname: ${{ env.VERACODE_APP_NAME }} + createprofile: true + filepath: app.zip + version: ${{ env.BUILD_VERSION }} + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + deleteincompletescan: "1" + waitForScan: false + + sca-scan: + name: Veracode SCA (Go modules) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Download modules (for SCA) + run: go mod download + + - name: SCA Agent Scan + env: + SRCCLR_API_TOKEN: ${{ secrets.SRCCLR_API_TOKEN }} + run: curl -sSL https://download.sourceclear.com/ci.sh | sh -s scan --recursive + continue-on-error: true diff --git a/templates/workflows/by-language/java-gradle.yml b/templates/workflows/by-language/java-gradle.yml new file mode 100644 index 0000000..ee875ee --- /dev/null +++ b/templates/workflows/by-language/java-gradle.yml @@ -0,0 +1,120 @@ +# ────────────────────────────────────────────────────────────────────────────── +# TEMPLATE: Java (Gradle) + Veracode +# BUILD: ./gradlew build → build/libs/*.jar or *.war +# SCANS: Pipeline Scan (PR) + Policy Scan (main) +# ────────────────────────────────────────────────────────────────────────────── +# REQUIRED SECRETS: VERACODE_API_ID, VERACODE_API_KEY +# CUSTOMIZE: VERACODE_APP_NAME, JAVA_VERSION, gradle task +# ────────────────────────────────────────────────────────────────────────────── + +name: Java Gradle + Veracode + +on: + push: + branches: [main, master, "release/**"] + pull_request: + types: [opened, synchronize, reopened] + +env: + VERACODE_APP_NAME: "YOUR_APP_NAME" # CUSTOMIZE + JAVA_VERSION: "17" # CUSTOMIZE: 8, 11, 17, 21 + BUILD_VERSION: "${{ github.ref_name }}-${{ github.run_number }}" + +jobs: + build: + name: Gradle Build + runs-on: ubuntu-latest + outputs: + artifact_path: ${{ steps.find-artifact.outputs.path }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Java ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: temurin + cache: gradle + + - name: Grant execute permission to gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: | + ./gradlew build -x test + # CUSTOMIZE: use 'bootJar' for Spring Boot, 'war' for web apps, etc. + + - name: Find built artifact + id: find-artifact + run: | + ARTIFACT=$(find build/libs/ -name "*.war" -o -name "*.jar" \ + | grep -v "sources\|plain\|javadoc" \ + | head -1) + echo "Found: $ARTIFACT" + echo "path=$ARTIFACT" >> $GITHUB_OUTPUT + + - uses: actions/upload-artifact@v4 + with: + name: gradle-artifact + path: ${{ steps.find-artifact.outputs.path }} + retention-days: 1 + + pipeline-scan: + name: Veracode Pipeline Scan + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: { name: gradle-artifact } + + - uses: veracode/pipeline-scan-action@v1 + with: + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + file: ${{ needs.build.outputs.artifact_path }} + fail_build: false + + - uses: actions/upload-artifact@v4 + if: always() + with: { name: pipeline-scan-results, path: results.json } + + policy-scan: + name: Veracode Policy Scan + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: { name: gradle-artifact } + + - uses: veracode/veracode-uploadandscan-action@master + with: + appname: ${{ env.VERACODE_APP_NAME }} + createprofile: true + filepath: ${{ needs.build.outputs.artifact_path }} + version: ${{ env.BUILD_VERSION }} + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + deleteincompletescan: "1" + waitForScan: false + + sca-scan: + name: Veracode SCA + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: temurin + - name: SCA Scan + env: + SRCCLR_API_TOKEN: ${{ secrets.SRCCLR_API_TOKEN }} + run: curl -sSL https://download.sourceclear.com/ci.sh | sh -s scan --recursive + continue-on-error: true diff --git a/templates/workflows/by-language/java-maven.yml b/templates/workflows/by-language/java-maven.yml new file mode 100644 index 0000000..f14aae1 --- /dev/null +++ b/templates/workflows/by-language/java-maven.yml @@ -0,0 +1,131 @@ +# ────────────────────────────────────────────────────────────────────────────── +# TEMPLATE: Java (Maven) + Veracode +# BUILD: mvn package → target/*.jar or *.war +# SCANS: Pipeline Scan (PR) + Policy Scan (main) +# ────────────────────────────────────────────────────────────────────────────── +# REQUIRED SECRETS: VERACODE_API_ID, VERACODE_API_KEY +# CUSTOMIZE: VERACODE_APP_NAME, JAVA_VERSION, maven goals +# ────────────────────────────────────────────────────────────────────────────── + +name: Java Maven + Veracode + +on: + push: + branches: [main, master, "release/**"] + pull_request: + types: [opened, synchronize, reopened] + +env: + VERACODE_APP_NAME: "YOUR_APP_NAME" # CUSTOMIZE + JAVA_VERSION: "17" # CUSTOMIZE: 8, 11, 17, 21 + BUILD_VERSION: "${{ github.ref_name }}-${{ github.run_number }}" + +jobs: + build: + name: Maven Build + runs-on: ubuntu-latest + outputs: + artifact_path: ${{ steps.find-artifact.outputs.path }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Java ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: temurin + cache: maven + + - name: Build with Maven + run: | + mvn -B package \ + -DskipTests \ + --file pom.xml + # CUSTOMIZE: add -P , -D=, etc. + + # For a WAR (web app), the artifact is typically target/*.war + # For a JAR (standalone), it's target/*.jar + # If multi-module: CUSTOMIZE the glob below + - name: Find built artifact + id: find-artifact + run: | + ARTIFACT=$(find target/ -name "*.war" -o -name "*.jar" \ + | grep -v "sources\|javadoc\|original" \ + | head -1) + echo "Found artifact: $ARTIFACT" + echo "path=$ARTIFACT" >> $GITHUB_OUTPUT + + - uses: actions/upload-artifact@v4 + with: + name: java-artifact + path: ${{ steps.find-artifact.outputs.path }} + retention-days: 1 + + pipeline-scan: + name: Veracode Pipeline Scan (SAST) + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: { name: java-artifact } + + - name: Pipeline Scan + uses: veracode/pipeline-scan-action@v1 + with: + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + file: ${{ needs.build.outputs.artifact_path }} + fail_build: false + # fail_on_severity: "Very High, High" + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: pipeline-scan-results + path: results.json + + policy-scan: + name: Veracode Policy Scan (SAST) + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: { name: java-artifact } + + - name: Policy Scan + uses: veracode/veracode-uploadandscan-action@master + with: + appname: ${{ env.VERACODE_APP_NAME }} + createprofile: true + filepath: ${{ needs.build.outputs.artifact_path }} + version: ${{ env.BUILD_VERSION }} + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + deleteincompletescan: "1" + waitForScan: false + + sca-scan: + name: Veracode SCA (Dependencies) + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: temurin + cache: maven + + - name: SCA Scan + env: + SRCCLR_API_TOKEN: ${{ secrets.SRCCLR_API_TOKEN }} + run: curl -sSL https://download.sourceclear.com/ci.sh | sh -s scan --recursive + continue-on-error: true diff --git a/templates/workflows/by-language/nodejs.yml b/templates/workflows/by-language/nodejs.yml new file mode 100644 index 0000000..0d1846f --- /dev/null +++ b/templates/workflows/by-language/nodejs.yml @@ -0,0 +1,131 @@ +# ────────────────────────────────────────────────────────────────────────────── +# TEMPLATE: Node.js / TypeScript + Veracode +# BUILD: npm ci + npm run build → zip source + dist +# SCANS: Pipeline Scan (PR) + Policy Scan (main) + SCA +# +# PACKAGING NOTE: +# Veracode scans Node.js source. Package all .js files (not node_modules) +# into a zip. See: https://docs.veracode.com/r/compilation_nodejs +# ────────────────────────────────────────────────────────────────────────────── +# REQUIRED SECRETS: VERACODE_API_ID, VERACODE_API_KEY, SRCCLR_API_TOKEN +# ────────────────────────────────────────────────────────────────────────────── + +name: Node.js + Veracode + +on: + push: + branches: [main, master, "release/**"] + pull_request: + types: [opened, synchronize, reopened] + +env: + VERACODE_APP_NAME: "YOUR_APP_NAME" # CUSTOMIZE + NODE_VERSION: "20" # CUSTOMIZE: 18, 20, 22 + BUILD_VERSION: "${{ github.ref_name }}-${{ github.run_number }}" + +jobs: + build: + name: Node.js Build & Package + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm # CUSTOMIZE: yarn or pnpm if applicable + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build 2>/dev/null || true # CUSTOMIZE: skip if no build step + + # Package source files for Veracode scanning + # CUSTOMIZE: adjust paths to include/exclude based on your project layout + - name: Package source for scan + run: | + zip -r app.zip . \ + -x "*.git*" \ + -x "node_modules/*" \ + -x "test/*" \ + -x "tests/*" \ + -x "spec/*" \ + -x "*.test.*" \ + -x "*.spec.*" \ + -x ".github/*" \ + -x "coverage/*" \ + -x ".nyc_output/*" + echo "Artifact size: $(du -sh app.zip)" + + - uses: actions/upload-artifact@v4 + with: + name: node-artifact + path: app.zip + retention-days: 1 + + pipeline-scan: + name: Veracode Pipeline Scan + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: { name: node-artifact } + + - uses: veracode/pipeline-scan-action@v1 + with: + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + file: app.zip + fail_build: false + + - uses: actions/upload-artifact@v4 + if: always() + with: { name: pipeline-scan-results, path: results.json } + + policy-scan: + name: Veracode Policy Scan + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: { name: node-artifact } + + - uses: veracode/veracode-uploadandscan-action@master + with: + appname: ${{ env.VERACODE_APP_NAME }} + createprofile: true + filepath: app.zip + version: ${{ env.BUILD_VERSION }} + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + deleteincompletescan: "1" + waitForScan: false + + sca-scan: + name: Veracode SCA (npm dependencies) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: SCA Agent Scan + env: + SRCCLR_API_TOKEN: ${{ secrets.SRCCLR_API_TOKEN }} + run: curl -sSL https://download.sourceclear.com/ci.sh | sh -s scan --recursive + continue-on-error: true diff --git a/templates/workflows/by-language/python.yml b/templates/workflows/by-language/python.yml new file mode 100644 index 0000000..7c715f8 --- /dev/null +++ b/templates/workflows/by-language/python.yml @@ -0,0 +1,132 @@ +# ────────────────────────────────────────────────────────────────────────────── +# TEMPLATE: Python + Veracode +# BUILD: zip source .py files +# SCANS: Pipeline Scan (PR) + Policy Scan (main) + SCA +# +# PACKAGING NOTE: +# Veracode scans Python source. Zip all .py files excluding tests/venv. +# See: https://docs.veracode.com/r/compilation_python +# ────────────────────────────────────────────────────────────────────────────── +# REQUIRED SECRETS: VERACODE_API_ID, VERACODE_API_KEY, SRCCLR_API_TOKEN +# ────────────────────────────────────────────────────────────────────────────── + +name: Python + Veracode + +on: + push: + branches: [main, master, "release/**"] + pull_request: + types: [opened, synchronize, reopened] + +env: + VERACODE_APP_NAME: "YOUR_APP_NAME" # CUSTOMIZE + PYTHON_VERSION: "3.11" # CUSTOMIZE: 3.9, 3.10, 3.11, 3.12 + BUILD_VERSION: "${{ github.ref_name }}-${{ github.run_number }}" + +jobs: + build: + name: Python Package for Scan + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + + - name: Install dependencies (for SCA and completeness) + run: | + pip install -r requirements.txt 2>/dev/null || \ + pip install -r requirements/base.txt 2>/dev/null || true + + # Package source for Veracode — exclude tests, venvs, cache + # CUSTOMIZE: adjust excludes based on your project structure + - name: Package Python source for scan + run: | + zip -r app.zip . \ + -x "*.git*" \ + -x "__pycache__/*" \ + -x "*.pyc" \ + -x ".venv/*" \ + -x "venv/*" \ + -x "env/*" \ + -x "tests/*" \ + -x "test/*" \ + -x "*.egg-info/*" \ + -x "dist/*" \ + -x "build/*" \ + -x ".github/*" \ + -x "htmlcov/*" \ + -x ".pytest_cache/*" + echo "Artifact size: $(du -sh app.zip)" + + - uses: actions/upload-artifact@v4 + with: + name: python-artifact + path: app.zip + retention-days: 1 + + pipeline-scan: + name: Veracode Pipeline Scan + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: { name: python-artifact } + + - uses: veracode/pipeline-scan-action@v1 + with: + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + file: app.zip + fail_build: false + + - uses: actions/upload-artifact@v4 + if: always() + with: { name: pipeline-scan-results, path: results.json } + + policy-scan: + name: Veracode Policy Scan + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: { name: python-artifact } + + - uses: veracode/veracode-uploadandscan-action@master + with: + appname: ${{ env.VERACODE_APP_NAME }} + createprofile: true + filepath: app.zip + version: ${{ env.BUILD_VERSION }} + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + deleteincompletescan: "1" + waitForScan: false + + sca-scan: + name: Veracode SCA (pip dependencies) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: pip install -r requirements.txt 2>/dev/null || true + + - name: SCA Agent Scan + env: + SRCCLR_API_TOKEN: ${{ secrets.SRCCLR_API_TOKEN }} + run: curl -sSL https://download.sourceclear.com/ci.sh | sh -s scan --recursive + continue-on-error: true diff --git a/templates/workflows/container-scan.yml b/templates/workflows/container-scan.yml new file mode 100644 index 0000000..6d816ea --- /dev/null +++ b/templates/workflows/container-scan.yml @@ -0,0 +1,145 @@ +# ────────────────────────────────────────────────────────────────────────────── +# TEMPLATE: Veracode Container / Image Security Scan +# SCAN TYPE: Container scanning — analyzes Docker image layers for vulnerabilities +# WHEN: On image build (push to main) or PR that changes Dockerfile +# RUNTIME: ~5–20 minutes +# DOCS: https://docs.veracode.com/r/c_container_scanning +# ────────────────────────────────────────────────────────────────────────────── +# REQUIRED SECRETS: +# VERACODE_API_ID - Veracode API Key ID +# VERACODE_API_KEY - Veracode API Key Secret +# +# OPTIONAL (for pushing to registry): +# REGISTRY_USERNAME - Container registry username +# REGISTRY_TOKEN - Container registry token/password +# ────────────────────────────────────────────────────────────────────────────── + +name: Veracode Container Scan + +on: + push: + branches: + - main + - master + paths: + - "Dockerfile" + - "Dockerfile.*" + - ".dockerignore" + - "docker-compose*.yml" + pull_request: + paths: + - "Dockerfile" + - "Dockerfile.*" + workflow_dispatch: + inputs: + image_ref: + description: "Docker image to scan (e.g. myapp:latest or registry/myapp:sha)" + required: false + type: string + +env: + # CUSTOMIZE: your image name and tag + IMAGE_NAME: "your-app-name" # TODO + IMAGE_TAG: ${{ github.sha }} + # CUSTOMIZE: container registry (ghcr.io, docker.io, etc.) + # REGISTRY: ghcr.io/${{ github.repository_owner }} + +jobs: + build-image: + name: Build Docker Image + runs-on: ubuntu-latest + outputs: + image_ref: ${{ steps.set-ref.outputs.image_ref }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # OPTIONAL: login to container registry + # - name: Log in to registry + # uses: docker/login-action@v3 + # with: + # registry: ${{ env.REGISTRY }} + # username: ${{ secrets.REGISTRY_USERNAME }} + # password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Build Docker image + run: | + # CUSTOMIZE: add build args, build context, etc. + docker build -t ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} . + + - name: Save image to tar for scanning + run: | + docker save ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \ + -o ${{ env.IMAGE_NAME }}-image.tar + + - name: Upload image tar + uses: actions/upload-artifact@v4 + with: + name: docker-image + path: ${{ env.IMAGE_NAME }}-image.tar + retention-days: 1 + + - id: set-ref + run: echo "image_ref=${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}" >> $GITHUB_OUTPUT + + container-scan: + name: Veracode Container Scan + runs-on: ubuntu-latest + needs: build-image + + steps: + - name: Download image tar + uses: actions/download-artifact@v4 + with: + name: docker-image + + - name: Load image + run: docker load -i ${{ env.IMAGE_NAME }}-image.tar + + # ── Option A: Veracode CLI container scan ───────────────────────────── + - name: Install Veracode CLI + run: curl -fsS https://tools.veracode.com/veracode-cli/install | sh + + - name: Veracode container scan + env: + VERACODE_API_KEY_ID: ${{ secrets.VERACODE_API_ID }} + VERACODE_API_KEY_SECRET: ${{ secrets.VERACODE_API_KEY }} + run: | + ./veracode scan \ + --type image \ + --source ${{ needs.build-image.outputs.image_ref }} \ + --format table + # CUSTOMIZE: add --project-name, --format json, --output results.json + continue-on-error: true # CUSTOMIZE: set to false to fail on findings + + # ── Option B: Veracode Container Security Action ────────────────────── + # Uncomment below and comment out Option A for the GitHub Action approach + # + # - name: Veracode Container Security Scan + # uses: veracode/container-security-action@v1 + # with: + # vid: ${{ secrets.VERACODE_API_ID }} + # vkey: ${{ secrets.VERACODE_API_KEY }} + # image: ${{ needs.build-image.outputs.image_ref }} + # # CUSTOMIZE: fail_build: true + # # CUSTOMIZE: min_cvss_for_fail: 7.0 + # # CUSTOMIZE: skip_fixable_only: false + + # ── Option C: Trivy + upload to GitHub Security (free alternative) ──── + # - name: Trivy vulnerability scan + # uses: aquasecurity/trivy-action@master + # with: + # image-ref: ${{ needs.build-image.outputs.image_ref }} + # format: sarif + # output: trivy-results.sarif + # - uses: github/codeql-action/upload-sarif@v3 + # with: { sarif_file: trivy-results.sarif } + + - name: Upload scan results + uses: actions/upload-artifact@v4 + if: always() + with: + name: container-scan-results + path: "*.json" + continue-on-error: true diff --git a/templates/workflows/dast-web-scan.yml b/templates/workflows/dast-web-scan.yml new file mode 100644 index 0000000..dae580c --- /dev/null +++ b/templates/workflows/dast-web-scan.yml @@ -0,0 +1,167 @@ +# ────────────────────────────────────────────────────────────────────────────── +# TEMPLATE: Veracode DAST Web Application Scan +# SCAN TYPE: DAST — Dynamic Analysis of a running web application +# WHEN: Manual trigger or scheduled (requires a live target URL) +# RUNTIME: Varies: 30 min – 8+ hours depending on app complexity +# DOCS: https://docs.veracode.com/r/c_was_intro +# ────────────────────────────────────────────────────────────────────────────── +# REQUIRED SECRETS: +# VERACODE_API_ID - Veracode API Key ID +# VERACODE_API_KEY - Veracode API Key Secret +# +# REQUIRED SETUP: +# 1. Your application must be running and reachable at TARGET_URL +# 2. For internal apps, configure an Internal Scan Management (ISM) gateway +# 3. Provide allowlist.csv and blocklist.csv (see Scripts/Release/ for format) +# +# TOOLS USED FROM THIS REPO: +# - Scripts/Release/DASTWebAppRequest-std.py (formats the scan request JSON) +# - Scripts/Release/DAST-ls-v2.sh (list/monitor scan status) +# ────────────────────────────────────────────────────────────────────────────── + +name: Veracode DAST Web Scan + +on: + # Manual trigger with configurable parameters + workflow_dispatch: + inputs: + target_url: + description: "Target URL to scan (e.g. https://app.example.com/)" + required: true + type: string + analysis_name: + description: "Analysis name in Veracode platform" + required: true + type: string + default: "GH-Actions-DAST" + org_email: + description: "Notification email for scan owner" + required: true + type: string + org_owner: + description: "Scan owner name" + required: false + default: "DevSecOps" + start_now: + description: "Start scan immediately?" + type: boolean + default: true + + # OPTIONAL: Scheduled scan (uncomment and customize cron) + # schedule: + # - cron: "0 2 * * 1" # Every Monday at 02:00 UTC + +env: + # CUSTOMIZE: base URL of this repo (to pull scripts without cloning) + SCRIPTS_REPO: "bnreplah/veracode-scripts" + SCRIPTS_BRANCH: "main" + +jobs: + dast-scan: + name: Configure & Submit DAST Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout this repo (or the target repo) + uses: actions/checkout@v4 + with: + # CUSTOMIZE: if running from your own repo, remove repository/ref below + # and ensure allowlist.csv / blacklist.csv are in the workspace root + repository: ${{ env.SCRIPTS_REPO }} + ref: ${{ env.SCRIPTS_BRANCH }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install httpie + Veracode HMAC signing + run: pip install httpie veracode-api-signing + + # ── Prepare allowlist / blocklist CSVs ────────────────────────────── + # CUSTOMIZE: replace these files with your application-specific lists. + # Format: directory_restriction_type,http_and_https,url + # Valid restriction types: NONE, FILE, FOLDER_ONLY, DIRECTORY_AND_SUBDIRECTORY + - name: Prepare scan configuration CSVs + run: | + # Create a minimal allowlist (CUSTOMIZE: add your app's URLs) + cat > allowlist.csv << 'EOF' + directory_restriction_type,http_and_https,url + DIRECTORY_AND_SUBDIRECTORY,TRUE,${{ github.event.inputs.target_url }} + EOF + + # Create a minimal blocklist (CUSTOMIZE: add URLs to exclude) + cat > blacklist.csv << 'EOF' + directory_restriction_type,http_and_https,url + NONE,TRUE,https://logout.example.com + EOF + + # ── Format the DAST analysis request JSON ────────────────────────── + - name: Format DAST request JSON + run: | + python Scripts/Release/DASTWebAppRequest-std.py \ + "${{ github.event.inputs.analysis_name }}" \ + "${{ github.event.inputs.target_url }}" \ + "${{ github.event.inputs.org_email }}" \ + "${{ github.event.inputs.org_owner }}" \ + > input.json + echo "Generated request:" + cat input.json | python3 -m json.tool + + # ── Submit the DAST scan to the Veracode platform ───────────────── + - name: Submit DAST analysis request + env: + VERACODE_API_KEY_ID: ${{ secrets.VERACODE_API_ID }} + VERACODE_API_KEY_SECRET: ${{ secrets.VERACODE_API_KEY }} + run: | + RESPONSE=$(http --auth-type veracode_hmac \ + POST "https://api.veracode.com/was/configservice/v1/analyses" \ + Content-Type:application/json \ + @input.json) + echo "API Response: $RESPONSE" + echo "$RESPONSE" > dast-submission-response.json + + # Extract the analysis ID from the response + ANALYSIS_ID=$(echo "$RESPONSE" | python3 -c \ + "import sys,json; d=json.load(sys.stdin); print(d.get('analysis_id','unknown'))" 2>/dev/null || echo "unknown") + echo "Analysis ID: $ANALYSIS_ID" + echo "ANALYSIS_ID=$ANALYSIS_ID" >> $GITHUB_ENV + + # ── Upload submission details ─────────────────────────────────────── + - name: Upload DAST submission artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: dast-scan-config + path: | + input.json + dast-submission-response.json + + - name: Summary + run: | + echo "## DAST Scan Submitted" >> $GITHUB_STEP_SUMMARY + echo "- **Analysis Name:** ${{ github.event.inputs.analysis_name }}" >> $GITHUB_STEP_SUMMARY + echo "- **Target URL:** ${{ github.event.inputs.target_url }}" >> $GITHUB_STEP_SUMMARY + echo "- **Analysis ID:** ${{ env.ANALYSIS_ID }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Monitor progress in the Veracode platform or run \`DAST-ls-v2.sh\`." >> $GITHUB_STEP_SUMMARY + + # ── OPTIONAL: Monitor DAST scan status ────────────────────────────────── + # Uncomment to poll for scan completion (may run for hours) + # + # monitor-dast-scan: + # name: Monitor DAST Scan + # runs-on: ubuntu-latest + # needs: dast-scan + # if: github.event.inputs.start_now == 'true' + # timeout-minutes: 480 # 8 hours max + # steps: + # - uses: actions/checkout@v4 + # with: { repository: "${{ env.SCRIPTS_REPO }}", ref: "${{ env.SCRIPTS_BRANCH }}" } + # - name: Install httpie + # run: pip install httpie veracode-api-signing + # - name: Poll scan status + # env: + # VERACODE_API_KEY_ID: ${{ secrets.VERACODE_API_ID }} + # VERACODE_API_KEY_SECRET: ${{ secrets.VERACODE_API_KEY }} + # run: bash Scripts/Release/DAST-ls-v2.sh diff --git a/templates/workflows/pipeline-scan.yml b/templates/workflows/pipeline-scan.yml new file mode 100644 index 0000000..8892bb7 --- /dev/null +++ b/templates/workflows/pipeline-scan.yml @@ -0,0 +1,100 @@ +# ────────────────────────────────────────────────────────────────────────────── +# TEMPLATE: Veracode Pipeline Scan +# SCAN TYPE: SAST (Static Analysis) — Fast inline scan, no platform policy gate +# WHEN: Every push and every pull request +# RUNTIME: ~2–5 minutes +# DOCS: https://docs.veracode.com/r/Pipeline_Scan +# ────────────────────────────────────────────────────────────────────────────── +# REQUIRED SECRETS (Settings → Secrets → Actions): +# VERACODE_API_ID - Veracode API Key ID +# VERACODE_API_KEY - Veracode API Key Secret +# +# ONBOARDING STEPS: +# 1. Replace every # CUSTOMIZE: comment with your actual values +# 2. Add the two secrets above to your repo +# 3. Push — the scan runs automatically on next commit/PR +# ────────────────────────────────────────────────────────────────────────────── + +name: Veracode Pipeline Scan + +on: + push: + branches: ["**"] + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build-and-pipeline-scan: + name: Build & Pipeline Scan + runs-on: ubuntu-latest # CUSTOMIZE: windows-latest or macos-latest if needed + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # ── BUILD STEP ────────────────────────────────────────────────────────── + # TODO: Replace this section with your actual build commands. + # The output must be a single uploadable artifact (jar/war/zip/dll/exe). + # See https://docs.veracode.com/r/compilation_packaging for packaging guides. + # + # Example — Java/Maven: + # - uses: actions/setup-java@v4 + # with: { java-version: '17', distribution: 'temurin' } + # - run: mvn -B package -DskipTests + # + # Example — Node.js (zip source): + # - uses: actions/setup-node@v4 + # with: { node-version: '20' } + # - run: npm ci && npm run build + # - run: zip -r app.zip . -x "*.git*" "node_modules/*" "test/*" + # + # Example — Python (zip source): + # - run: zip -r app.zip . -x "*.git*" "__pycache__/*" "*.pyc" "tests/*" + # + # Example — .NET: + # - uses: actions/setup-dotnet@v4 + # with: { dotnet-version: '8.0.x' } + # - run: dotnet publish -c Release -o publish/ + # - run: zip -r app.zip publish/ + + - name: TODO - Add your build step here + run: echo "Replace this step with your actual build command" # TODO + + # ── PIPELINE SCAN ─────────────────────────────────────────────────────── + - name: Veracode Pipeline Scan + uses: veracode/pipeline-scan-action@v1 + with: + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + # CUSTOMIZE: path to your compiled artifact + file: "app.zip" + # CUSTOMIZE: set to true to fail the build when findings exceed threshold + fail_build: false + # CUSTOMIZE: comma-separated severities that fail the build (when fail_build=true) + # fail_on_severity: "Very High, High" + # CUSTOMIZE: only report findings not in a previous baseline + # baseline_file: "baseline.json" + # CUSTOMIZE: policy name to evaluate against (optional) + # request_policy: "Veracode Recommended Medium + SCA" + id: pipeline-scan + + # ── RESULTS ───────────────────────────────────────────────────────────── + - name: Upload pipeline scan results + uses: actions/upload-artifact@v4 + if: always() + with: + name: pipeline-scan-results + path: | + results.json + filtered_results.json + + # OPTIONAL: Import findings as GitHub code scanning alerts (requires SARIF) + # - name: Convert to SARIF and upload + # uses: veracode/veracode-pipeline-scan-results-to-sarif@v2 + # with: + # pipeline-results-json: results.json + # output-results-sarif: veracode-results.sarif + # finding-rule-level: "3:1:0" + # - uses: github/codeql-action/upload-sarif@v3 + # with: + # sarif_file: veracode-results.sarif diff --git a/templates/workflows/policy-scan-sast.yml b/templates/workflows/policy-scan-sast.yml new file mode 100644 index 0000000..25c7c6f --- /dev/null +++ b/templates/workflows/policy-scan-sast.yml @@ -0,0 +1,138 @@ +# ────────────────────────────────────────────────────────────────────────────── +# TEMPLATE: Veracode Policy / Full SAST Scan +# SCAN TYPE: SAST (Static Analysis) — Full scan, evaluated against policy +# WHEN: Pushes to main/release branches; not on every PR (long runtime) +# RUNTIME: 15 min – several hours depending on app size +# DOCS: https://docs.veracode.com/r/c_uploadandscan +# ────────────────────────────────────────────────────────────────────────────── +# REQUIRED SECRETS: +# VERACODE_API_ID - Veracode API Key ID +# VERACODE_API_KEY - Veracode API Key Secret +# +# ONBOARDING STEPS: +# 1. Set VERACODE_APP_NAME to match your application's profile in the platform +# 2. Replace the build step with your actual build commands +# 3. Set ARTIFACT_PATH to point to your compiled artifact +# 4. Add secrets to your repo and push +# ────────────────────────────────────────────────────────────────────────────── + +name: Veracode Policy Scan (SAST) + +on: + push: + branches: + - main + - master + - "release/**" # CUSTOMIZE: add other protected branches + workflow_dispatch: # Allow manual trigger + inputs: + sandbox: + description: "Sandbox name (leave blank for policy scan)" + required: false + default: "" + +env: + # CUSTOMIZE: name of your application profile in the Veracode platform + VERACODE_APP_NAME: "YOUR_APP_NAME" + # CUSTOMIZE: path to the artifact produced by your build step + ARTIFACT_PATH: "app.zip" + # CUSTOMIZE: human-readable build/version label + BUILD_VERSION: "${{ github.ref_name }}-${{ github.run_number }}" + +jobs: + build: + name: Build Application + runs-on: ubuntu-latest # CUSTOMIZE: match your build environment + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # TODO: Replace with your actual build steps (see pipeline-scan.yml for examples) + - name: TODO - Add your build step + run: echo "Replace with your build command" # TODO + + - name: Upload artifact for scan job + uses: actions/upload-artifact@v4 + with: + name: scan-artifact + path: ${{ env.ARTIFACT_PATH }} + retention-days: 1 + + sast-policy-scan: + name: Veracode SAST Policy Scan + runs-on: ubuntu-latest + needs: build + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: scan-artifact + + # ── Option A: Official GitHub Action (recommended) ──────────────────── + - name: Veracode Upload and Scan + uses: veracode/veracode-uploadandscan-action@master + with: + appname: ${{ env.VERACODE_APP_NAME }} + createprofile: true # Creates app profile in platform if missing + filepath: ${{ env.ARTIFACT_PATH }} + version: ${{ env.BUILD_VERSION }} + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + # CUSTOMIZE: sandbox name from workflow_dispatch input, or hardcode one + sandboxname: ${{ github.event.inputs.sandbox || '' }} + # CUSTOMIZE: delete an incomplete scan to unblock (0=cancel, 1=delete) + deleteincompletescan: "1" + # CUSTOMIZE: set true to wait for scan completion and check policy + waitForScan: true + # CUSTOMIZE: set true to fail the build if policy is not passed + failbuild: false + + # ── Option B: Java Wrapper via this repo's installer (alternative) ───── + # Uncomment below and comment out Option A if you prefer the Java wrapper. + # + # - name: Install Veracode Java API Wrapper + # run: | + # bash <(curl -fsSL https://raw.githubusercontent.com/bnreplah/veracode-scripts/main/Scripts/Release/veracode-installer.sh) \ + # --install-java-api-wrapper + # + # - name: Upload and Scan + # env: + # VERACODE_API_ID: ${{ secrets.VERACODE_API_ID }} + # VERACODE_API_KEY: ${{ secrets.VERACODE_API_KEY }} + # run: | + # java -jar VeracodeJavaAPI.jar \ + # -action uploadandscan \ + # -filepath ${{ env.ARTIFACT_PATH }} \ + # -appname "${{ env.VERACODE_APP_NAME }}" \ + # -createprofile true \ + # -version "${{ env.BUILD_VERSION }}" \ + # -deleteincompletescan 1 + + # ── OPTIONAL: Import findings to GitHub Security tab ───────────────────────── + import-findings: + name: Import Findings to GitHub + runs-on: ubuntu-latest + needs: sast-policy-scan + if: always() + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # OPTIONAL: pull findings into GitHub Security > Code scanning alerts + # - name: Veracode Flaw Importer + # uses: veracode/veracode-flaw-importer-action@main + # with: + # vid: ${{ secrets.VERACODE_API_ID }} + # vkey: ${{ secrets.VERACODE_API_KEY }} + # type: github + # appname: ${{ env.VERACODE_APP_NAME }} + # sandboxname: "" + # scantype: Dynamic,Static,SCA + + - name: Scan complete + run: echo "Policy scan submitted for ${{ env.VERACODE_APP_NAME }} build ${{ env.BUILD_VERSION }}" diff --git a/templates/workflows/sandbox-scan-promote.yml b/templates/workflows/sandbox-scan-promote.yml new file mode 100644 index 0000000..f1c5d86 --- /dev/null +++ b/templates/workflows/sandbox-scan-promote.yml @@ -0,0 +1,154 @@ +# ────────────────────────────────────────────────────────────────────────────── +# TEMPLATE: Veracode Sandbox Scan + Promote to Policy +# SCAN TYPE: SAST — Sandbox (feature branch) → promote to policy on merge +# WHEN: Feature branches scan to sandbox; main branch promotes + policy scan +# RUNTIME: 15 min – several hours +# DOCS: https://docs.veracode.com/r/c_about_sandbox +# ────────────────────────────────────────────────────────────────────────────── +# WORKFLOW: +# feature/* push → scan in sandbox (named after branch) +# PR to main → also scans in sandbox with PR label +# push to main → promote sandbox findings to policy, then policy scan +# +# REQUIRED SECRETS: +# VERACODE_API_ID - Veracode API Key ID +# VERACODE_API_KEY - Veracode API Key Secret +# ────────────────────────────────────────────────────────────────────────────── + +name: Veracode Sandbox Scan & Promote + +on: + push: + branches: + - "feature/**" + - "bugfix/**" + - "hotfix/**" + - main + - master + pull_request: + branches: + - main + - master + +env: + # CUSTOMIZE: your Veracode application profile name + VERACODE_APP_NAME: "YOUR_APP_NAME" + ARTIFACT_PATH: "app.zip" + # Sandbox is named after the branch (sanitized for Veracode) + SANDBOX_NAME: ${{ github.head_ref || github.ref_name }} + BUILD_VERSION: "${{ github.ref_name }}-${{ github.run_number }}" + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # TODO: Replace with your actual build commands + - name: TODO - Add build step + run: echo "Replace with your build command" + + - uses: actions/upload-artifact@v4 + with: + name: scan-artifact + path: ${{ env.ARTIFACT_PATH }} + retention-days: 1 + + # ── Sandbox scan for feature branches and PRs ───────────────────────────── + sandbox-scan: + name: Sandbox SAST Scan + runs-on: ubuntu-latest + needs: build + # Run on feature branches and pull requests (not on main direct push) + if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master' + + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: scan-artifact + + - name: Veracode Sandbox Scan + uses: veracode/veracode-uploadandscan-action@master + with: + appname: ${{ env.VERACODE_APP_NAME }} + createprofile: true + filepath: ${{ env.ARTIFACT_PATH }} + version: ${{ env.BUILD_VERSION }} + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + sandboxname: ${{ env.SANDBOX_NAME }} + createsandbox: true # Create sandbox if it doesn't exist + deleteincompletescan: "1" + waitForScan: true + failbuild: false # CUSTOMIZE: set true to enforce sandbox policy + + # ── Promote sandbox + full policy scan on merge to main ─────────────────── + promote-and-policy-scan: + name: Promote Sandbox & Policy Scan + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: scan-artifact + + # Step 1: promote sandbox to policy (using this repo's promote script) + # CUSTOMIZE: set PR_SANDBOX_NAME to the name of the sandbox to promote + - name: Promote sandbox to policy + env: + VERACODE_API_ID: ${{ secrets.VERACODE_API_ID }} + VERACODE_API_KEY: ${{ secrets.VERACODE_API_KEY }} + run: | + pip install httpie veracode-api-signing 2>/dev/null || true + # CUSTOMIZE: replace "feature/my-branch" with the sandbox to promote + APP_NAME="${{ env.VERACODE_APP_NAME }}" + SANDBOX_NAME="${{ github.event.pull_request.head.ref || 'dev' }}" + + APP_GUID=$(http --auth-type veracode_hmac \ + "https://api.veracode.com/appsec/v1/applications?name=${APP_NAME}" \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['_embedded']['applications'][0]['guid'])" 2>/dev/null || echo "") + + if [ -n "$APP_GUID" ]; then + SANDBOX_GUID=$(http --auth-type veracode_hmac \ + "https://api.veracode.com/appsec/v1/applications/${APP_GUID}/sandboxes" \ + | python3 -c " + import sys,json + d=json.load(sys.stdin) + name='${SANDBOX_NAME}' + sandboxes=d.get('_embedded',{}).get('sandboxes',[]) + match=[s['guid'] for s in sandboxes if s['name']==name] + print(match[0] if match else '') + " 2>/dev/null || echo "") + + if [ -n "$SANDBOX_GUID" ]; then + echo "Promoting sandbox '$SANDBOX_NAME' (${SANDBOX_GUID}) to policy..." + http --auth-type veracode_hmac POST \ + "https://api.veracode.com/appsec/v1/applications/${APP_GUID}/sandboxes/${SANDBOX_GUID}/promote" + else + echo "Sandbox '$SANDBOX_NAME' not found — skipping promotion" + fi + else + echo "Application '$APP_NAME' not found — skipping promotion" + fi + continue-on-error: true + + # Step 2: full policy scan on main + - name: Veracode Policy Scan + uses: veracode/veracode-uploadandscan-action@master + with: + appname: ${{ env.VERACODE_APP_NAME }} + createprofile: true + filepath: ${{ env.ARTIFACT_PATH }} + version: ${{ env.BUILD_VERSION }} + vid: ${{ secrets.VERACODE_API_ID }} + vkey: ${{ secrets.VERACODE_API_KEY }} + deleteincompletescan: "1" + waitForScan: true + failbuild: false # CUSTOMIZE: set true to gate deployment on policy pass diff --git a/templates/workflows/sca-agent-scan.yml b/templates/workflows/sca-agent-scan.yml new file mode 100644 index 0000000..1260b83 --- /dev/null +++ b/templates/workflows/sca-agent-scan.yml @@ -0,0 +1,113 @@ +# ────────────────────────────────────────────────────────────────────────────── +# TEMPLATE: Veracode SCA (Software Composition Analysis) Agent Scan +# SCAN TYPE: SCA — identifies vulnerable open-source libraries +# WHEN: Every PR + pushes to main (catches new vulnerable dependencies early) +# RUNTIME: ~2–10 minutes +# DOCS: https://docs.veracode.com/r/c_sc_agent_scan +# ────────────────────────────────────────────────────────────────────────────── +# REQUIRED SECRETS: +# SRCCLR_API_TOKEN - SCA Agent token (from Veracode platform → Integrations → SCA) +# +# OPTIONAL — for correlating SCA findings with an app profile: +# VERACODE_API_ID +# VERACODE_API_KEY +# +# ONBOARDING STEPS: +# 1. Create an SCA workspace and agent in the Veracode platform +# 2. Copy the agent token to SRCCLR_API_TOKEN secret +# 3. Customize scan flags in the srcclr scan command below +# ────────────────────────────────────────────────────────────────────────────── + +name: Veracode SCA Scan + +on: + push: + branches: + - main + - master + - "release/**" + pull_request: + types: [opened, synchronize, reopened] + +jobs: + sca-scan: + name: SCA Agent Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # ── Language runtime setup ─────────────────────────────────────────── + # TODO: Uncomment the runtime that matches your project + # + # Java: + # - uses: actions/setup-java@v4 + # with: { java-version: '17', distribution: 'temurin' } + # + # Node.js: + # - uses: actions/setup-node@v4 + # with: { node-version: '20', cache: 'npm' } + # - run: npm ci + # + # Python: + # - uses: actions/setup-python@v5 + # with: { python-version: '3.11' } + # - run: pip install -r requirements.txt + # + # .NET: + # - uses: actions/setup-dotnet@v4 + # with: { dotnet-version: '8.0.x' } + # - run: dotnet restore + # + # Go: + # - uses: actions/setup-go@v5 + # with: { go-version: '1.22' } + + # ── SCA agent scan ─────────────────────────────────────────────────── + - name: Veracode SCA Agent Scan + env: + SRCCLR_API_TOKEN: ${{ secrets.SRCCLR_API_TOKEN }} + run: | + curl -sSL https://download.sourceclear.com/ci.sh | sh -s scan \ + --allow-dirty \ + --recursive \ + --skip-collectors none + # CUSTOMIZE additional flags: + # --update-advisor show remediation guidance + # --vuln-methods detect vulnerable methods (Java) + # --no-graphs skip dependency graphs + # --json=results.json write JSON report + + # ── Upload SCA results as artifact ─────────────────────────────────── + - name: Upload SCA results + uses: actions/upload-artifact@v4 + if: always() + with: + name: sca-results + path: | + scaResults.json + results.json + continue-on-error: true + + # ── Optional: SCA scan via Veracode platform (links to app profile) ────── + # Uncomment to also run a platform-linked SCA scan (requires VERACODE_API_ID/KEY) + # + # sca-platform-scan: + # name: SCA Platform Scan + # runs-on: ubuntu-latest + # needs: sca-scan + # if: github.ref == 'refs/heads/main' + # steps: + # - uses: actions/checkout@v4 + # - name: Install Veracode CLI + # run: curl -fsS https://tools.veracode.com/veracode-cli/install | sh + # - name: SCA scan via Veracode CLI + # env: + # VERACODE_API_KEY_ID: ${{ secrets.VERACODE_API_ID }} + # VERACODE_API_KEY_SECRET: ${{ secrets.VERACODE_API_KEY }} + # run: | + # ./veracode scan \ + # --type directory \ + # --source . \ + # --project-name "YOUR_APP_NAME" # CUSTOMIZE diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..9b163c0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,49 @@ +""" +Shared pytest fixtures and configuration for the Veracode SDK test suite. +""" + +import shutil +import pytest +from pathlib import Path + +REPO_ROOT = Path(__file__).parent.parent +SCRIPTS_DIR = REPO_ROOT / "Scripts" +RELEASE_DIR = SCRIPTS_DIR / "Release" +DEV_DIR = SCRIPTS_DIR / "Dev" +FIXTURES_DIR = Path(__file__).parent / "fixtures" + + +@pytest.fixture(scope="session") +def repo_root(): + """Return the repository root path.""" + return REPO_ROOT + + +@pytest.fixture(scope="session") +def release_dir(): + """Return the path to the Release scripts directory.""" + return RELEASE_DIR + + +@pytest.fixture(scope="session") +def dev_dir(): + """Return the path to the Dev scripts directory.""" + return DEV_DIR + + +@pytest.fixture(scope="session") +def fixtures_dir(): + """Return the path to the test fixtures directory.""" + return FIXTURES_DIR + + +@pytest.fixture +def tmp_work_dir(tmp_path): + """ + Create a temp working directory pre-populated with all test CSV fixtures. + Use this for running scripts that need allowlist.csv, blacklist.csv, glblacklist.csv + in the current working directory. + """ + for csv_file in FIXTURES_DIR.glob("*.csv"): + shutil.copy(csv_file, tmp_path / csv_file.name) + return tmp_path diff --git a/tests/fixtures/allowlist.csv b/tests/fixtures/allowlist.csv new file mode 100644 index 0000000..ce42d36 --- /dev/null +++ b/tests/fixtures/allowlist.csv @@ -0,0 +1,5 @@ +directory_restriction_type,http_and_https,url +NONE,TRUE,https://example.com +DIRECTORY_AND_SUBDIRECTORY,TRUE,https://api.example.com +FOLDER_ONLY,TRUE,https://www.example.com +FILE,TRUE,https://docs.example.com diff --git a/tests/fixtures/blacklist.csv b/tests/fixtures/blacklist.csv new file mode 100644 index 0000000..65abf29 --- /dev/null +++ b/tests/fixtures/blacklist.csv @@ -0,0 +1,4 @@ +directory_restriction_type,http_and_https,url +NONE,TRUE,https://blocked.example.com +FILE,FALSE,https://private.example.com +DIRECTORY_AND_SUBDIRECTORY,TRUE,https://secret.example.com diff --git a/tests/fixtures/glblacklist.csv b/tests/fixtures/glblacklist.csv new file mode 100644 index 0000000..3ad9522 --- /dev/null +++ b/tests/fixtures/glblacklist.csv @@ -0,0 +1,3 @@ +directory_restriction_type,http_and_https,url +NONE,TRUE,https://global-blocked.example.com +DIRECTORY_AND_SUBDIRECTORY,FALSE,https://internal.example.com diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/test_api_connectivity.py b/tests/integration/test_api_connectivity.py new file mode 100644 index 0000000..88ab519 --- /dev/null +++ b/tests/integration/test_api_connectivity.py @@ -0,0 +1,143 @@ +""" +Veracode API connectivity integration tests. + +These tests make LIVE calls to the Veracode REST/XML APIs and require +valid credentials to be configured via one of: + - Environment variables: VERACODE_API_ID and VERACODE_API_KEY + - Credentials file: ~/.veracode/credentials + +Mark: @pytest.mark.api +Skip: Automatically skipped if no credentials are found. + +Run selectively with: pytest tests/integration/test_api_connectivity.py -m api -v +""" + +import os +import subprocess +import sys +import shutil +import json +import pytest +from pathlib import Path + +pytestmark = pytest.mark.api + +RELEASE_DIR = Path(__file__).parent.parent.parent / "Scripts" / "Release" +FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" + +CREDS_FILE = Path.home() / ".veracode" / "credentials" + + +def credentials_available() -> bool: + """Return True if Veracode API credentials can be found.""" + has_env = bool( + os.environ.get("VERACODE_API_ID") and os.environ.get("VERACODE_API_KEY") + ) + return has_env or CREDS_FILE.exists() + + +skip_no_creds = pytest.mark.skipif( + not credentials_available(), + reason=( + "Veracode API credentials not found. " + "Set VERACODE_API_ID + VERACODE_API_KEY env vars " + "or configure ~/.veracode/credentials" + ), +) + + +@skip_no_creds +class TestDASTAPIConnectivity: + """Tests that verify connectivity to the Veracode Dynamic Analysis API.""" + + def test_dast_ls_returns_without_auth_error(self): + """DAST-ls-v2.sh should connect and not return a 401 auth error.""" + script = RELEASE_DIR / "DAST-ls-v2.sh" + if not script.exists(): + pytest.skip("DAST-ls-v2.sh not found") + + result = subprocess.run( + ["bash", str(script)], + capture_output=True, + text=True, + timeout=60, + env={**os.environ}, + ) + combined = result.stdout + result.stderr + assert "401" not in combined, f"Authentication error returned:\n{combined}" + assert "Unauthorized" not in combined + + def test_dast_ls_returns_json_or_status(self): + """DAST-ls-v2.sh should return JSON data or an API status message.""" + script = RELEASE_DIR / "DAST-ls-v2.sh" + if not script.exists(): + pytest.skip("DAST-ls-v2.sh not found") + + result = subprocess.run( + ["bash", str(script)], + capture_output=True, + text=True, + timeout=60, + ) + # Either valid JSON response or a recognized API response structure + combined = result.stdout + result.stderr + assert len(combined.strip()) > 0, "No output from DAST ls script" + + +@skip_no_creds +class TestSASTAPIConnectivity: + """Tests that verify connectivity to the Veracode Static Analysis API.""" + + def test_search_build_no_auth_error(self): + """SearchBuildByName.sh should connect without auth errors.""" + script = RELEASE_DIR / "SearchBuildByName.sh" + if not script.exists(): + pytest.skip("SearchBuildByName.sh not found") + + result = subprocess.run( + ["bash", str(script), "connectivity-test-app"], + capture_output=True, + text=True, + timeout=60, + input="", + ) + combined = result.stdout + result.stderr + assert "401" not in combined, f"Auth error:\n{combined}" + + +@skip_no_creds +class TestDASTRequestSubmission: + """ + Tests that attempt to format and validate a DAST analysis request + against the Veracode API schema. + + NOTE: These tests format a request JSON but do NOT submit/create scans + to avoid affecting production data. + """ + + def test_formatted_request_valid_json(self, tmp_path): + """A formatted DAST request should produce valid JSON output.""" + shutil.copy(FIXTURES_DIR / "allowlist.csv", tmp_path / "allowlist.csv") + shutil.copy(FIXTURES_DIR / "blacklist.csv", tmp_path / "blacklist.csv") + + script = RELEASE_DIR / "DASTWebAppRequest-std.py" + result = subprocess.run( + [ + sys.executable, + str(script), + "api-connectivity-test", + "https://target.example.com/", + os.environ.get("VERACODE_ORG_EMAIL", "test@example.com"), + "API Test", + ], + capture_output=True, + text=True, + cwd=str(tmp_path), + timeout=30, + ) + non_empty = [l for l in result.stdout.strip().splitlines() if l.strip()] + assert non_empty, f"No output. stderr: {result.stderr}" + + parsed = json.loads(non_empty[-1]) + assert "name" in parsed + assert "scans" in parsed diff --git a/tests/integration/test_blacklist_script.py b/tests/integration/test_blacklist_script.py new file mode 100644 index 0000000..926ccbd --- /dev/null +++ b/tests/integration/test_blacklist_script.py @@ -0,0 +1,128 @@ +""" +Integration tests for Scripts/Release/BlackList-std.py. + +Tests the script end-to-end via subprocess, verifying: + - Script exits successfully when CSV files are present + - Output contains expected scan configuration fragments + - Blacklist entries from the CSV appear in the output + - input.json is written to the working directory + +These tests do NOT require Veracode API credentials. +The script runs its built-in test() function when DEBUG=True (the default). +""" + +import csv +import sys +import shutil +import subprocess +import pytest +from pathlib import Path + +RELEASE_DIR = Path(__file__).parent.parent.parent / "Scripts" / "Release" +FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" +SCRIPT = RELEASE_DIR / "BlackList-std.py" + + +@pytest.fixture +def work_dir(tmp_path): + """Temp dir with CSV fixtures so the script can find blacklist files.""" + for f in FIXTURES_DIR.glob("*.csv"): + shutil.copy(f, tmp_path / f.name) + return tmp_path + + +def run_script(work_dir): + return subprocess.run( + [sys.executable, str(SCRIPT)], + capture_output=True, + text=True, + cwd=str(work_dir), + ) + + +# --- Execution health --- + +class TestBlacklistScriptExecution: + def test_exits_zero(self, work_dir): + result = run_script(work_dir) + assert result.returncode == 0, f"Non-zero exit:\nstdout: {result.stdout}\nstderr: {result.stderr}" + + def test_produces_output(self, work_dir): + result = run_script(work_dir) + assert result.stdout.strip() != "" + + def test_writes_input_json(self, work_dir): + run_script(work_dir) + assert (work_dir / "input.json").exists() + + def test_input_json_is_nonempty(self, work_dir): + run_script(work_dir) + content = (work_dir / "input.json").read_text() + assert content.strip() != "" + + +# --- Output content --- + +class TestBlacklistScriptOutput: + def test_output_contains_analysis_name_prefix(self, work_dir): + """The test() function prefixes the name with 'veracode-api-test-'.""" + result = run_script(work_dir) + assert "veracode-api-test-" in result.stdout + + def test_output_contains_scan_config(self, work_dir): + result = run_script(work_dir) + assert "scan_config_request" in result.stdout + + def test_output_contains_target_url(self, work_dir): + result = run_script(work_dir) + assert "target_url" in result.stdout + + def test_output_contains_veracode_test_url(self, work_dir): + """The test() function uses http://veracode.com as the target URL.""" + result = run_script(work_dir) + assert "veracode.com" in result.stdout + + def test_output_contains_org_email(self, work_dir): + """The test() function uses example@example.com as the org email.""" + result = run_script(work_dir) + assert "example@example.com" in result.stdout + + def test_output_contains_blacklist_config(self, work_dir): + result = run_script(work_dir) + assert "blacklist_configuration" in result.stdout or "black_list" in result.stdout + + def test_blacklist_urls_appear_in_output(self, work_dir): + """URLs from blacklist.csv should be present in the output.""" + with open(FIXTURES_DIR / "blacklist.csv") as f: + reader = csv.DictReader(f) + urls = [row["url"].strip() for row in reader] + + result = run_script(work_dir) + assert any(url in result.stdout for url in urls), ( + f"None of the blacklist URLs {urls} found in output:\n{result.stdout[:500]}" + ) + + +# --- CSV not found handling --- + +class TestBlacklistMissingCSV: + def test_exits_zero_without_csv_files(self, tmp_path): + """Script should not crash when CSV files are missing (graceful error handling).""" + result = subprocess.run( + [sys.executable, str(SCRIPT)], + capture_output=True, + text=True, + cwd=str(tmp_path), + ) + assert result.returncode == 0 + + def test_reports_csv_load_error_without_csv(self, tmp_path): + """When CSV files are missing the script should report load failures.""" + result = subprocess.run( + [sys.executable, str(SCRIPT)], + capture_output=True, + text=True, + cwd=str(tmp_path), + ) + combined = result.stdout + result.stderr + assert "failed to load" in combined or "veracode-api-test-" in combined diff --git a/tests/integration/test_dast_web_request_script.py b/tests/integration/test_dast_web_request_script.py new file mode 100644 index 0000000..35c1428 --- /dev/null +++ b/tests/integration/test_dast_web_request_script.py @@ -0,0 +1,190 @@ +""" +Integration tests for Scripts/Release/DASTWebAppRequest-std.py. + +Tests the script end-to-end via subprocess, verifying: + - stdout mode with CLI args produces valid JSON + - Request structure matches Veracode Dynamic Analysis API schema + - Input file (input.json) is written correctly + - Org info, scan config, and schedule are included + +These tests do NOT require Veracode API credentials. +The script is invoked with pre-set CLI args so it runs non-interactively. +""" + +import json +import sys +import shutil +import subprocess +import pytest +from pathlib import Path + +RELEASE_DIR = Path(__file__).parent.parent.parent / "Scripts" / "Release" +FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" +SCRIPT = RELEASE_DIR / "DASTWebAppRequest-std.py" + +TEST_ARGS = [ + "integration-test-analysis", + "https://target.example.com/", + "owner@example.com", + "Test Owner", +] + + +@pytest.fixture +def work_dir(tmp_path): + """Temp dir with CSV fixtures so the script can find allowlist/blocklist files.""" + for f in FIXTURES_DIR.glob("*.csv"): + shutil.copy(f, tmp_path / f.name) + return tmp_path + + +def run_script(work_dir, args=None): + """Run DASTWebAppRequest-std.py from work_dir with the given args.""" + cmd = [sys.executable, str(SCRIPT)] + (args or TEST_ARGS) + return subprocess.run(cmd, capture_output=True, text=True, cwd=str(work_dir)) + + +def extract_json(result) -> dict: + """Extract the last JSON object from the script's stdout.""" + non_empty = [l for l in result.stdout.strip().splitlines() if l.strip()] + assert non_empty, f"No output from script. stderr: {result.stderr}" + return json.loads(non_empty[-1]) + + +# --- Basic execution --- + +class TestScriptExecution: + def test_exits_zero(self, work_dir): + result = run_script(work_dir) + assert result.returncode == 0, f"Non-zero exit: {result.stderr}" + + def test_produces_stdout(self, work_dir): + result = run_script(work_dir) + assert result.stdout.strip() != "" + + def test_stdout_ends_with_valid_json(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + assert isinstance(parsed, dict) + + def test_writes_input_json_file(self, work_dir): + run_script(work_dir) + assert (work_dir / "input.json").exists() + + def test_input_json_matches_stdout_json(self, work_dir): + result = run_script(work_dir) + stdout_parsed = extract_json(result) + with open(work_dir / "input.json") as f: + file_parsed = json.load(f) + assert stdout_parsed == file_parsed + + +# --- Request structure --- + +class TestRequestStructure: + def test_name_field_present(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + assert "name" in parsed + + def test_name_matches_arg(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + assert parsed["name"] == TEST_ARGS[0] + + def test_scans_field_present(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + assert "scans" in parsed + + def test_scans_is_nonempty_list(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + assert isinstance(parsed["scans"], list) + assert len(parsed["scans"]) >= 1 + + def test_schedule_field_present(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + assert "schedule" in parsed + + def test_schedule_has_duration(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + assert "duration" in parsed["schedule"] + + def test_org_info_present(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + assert "org_info" in parsed + + def test_org_info_has_email(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + assert "email" in parsed["org_info"] + + def test_org_email_matches_arg(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + assert parsed["org_info"]["email"] == TEST_ARGS[2] + + +# --- Scan configuration --- + +class TestScanConfiguration: + def test_scan_has_scan_config_request(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + scan = parsed["scans"][0] + assert "scan_config_request" in scan + + def test_scan_config_has_target_url(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + scan_config = parsed["scans"][0]["scan_config_request"] + assert "target_url" in scan_config + + def test_target_url_matches_arg(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + target = parsed["scans"][0]["scan_config_request"]["target_url"] + assert target["url"] == TEST_ARGS[1] + + def test_target_url_has_http_and_https(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + target = parsed["scans"][0]["scan_config_request"]["target_url"] + assert "http_and_https" in target + + def test_http_and_https_is_true(self, work_dir): + result = run_script(work_dir) + parsed = extract_json(result) + target = parsed["scans"][0]["scan_config_request"]["target_url"] + # The script sets this to "true" (string) or boolean true + assert str(target["http_and_https"]).lower() == "true" + + +# --- Different analysis names --- + +class TestAnalysisNameVariants: + @pytest.mark.parametrize("name", [ + "my-app-dast-scan", + "prod-api-scan-v2", + "veracode-weekly-analysis", + ]) + def test_custom_analysis_name(self, work_dir, name): + args = [name, "https://example.com/", "scan@company.com", "Owner"] + result = run_script(work_dir, args) + parsed = extract_json(result) + assert parsed["name"] == name + + @pytest.mark.parametrize("url", [ + "https://app.example.com/", + "https://api.example.com/v1/", + "https://staging.example.com/", + ]) + def test_various_target_urls(self, work_dir, url): + args = ["test-scan", url, "test@example.com", "Owner"] + result = run_script(work_dir, args) + parsed = extract_json(result) + assert parsed["scans"][0]["scan_config_request"]["target_url"]["url"] == url diff --git a/tests/integration/test_shell_scripts.py b/tests/integration/test_shell_scripts.py new file mode 100644 index 0000000..a035287 --- /dev/null +++ b/tests/integration/test_shell_scripts.py @@ -0,0 +1,223 @@ +""" +Integration tests for shell scripts under Scripts/Release/ and Scripts/Dev/bash_scripts/. + +Tests: + - bash -n syntax validation for all .sh files + - shellcheck linting (skipped if shellcheck not installed) + - Functional smoke tests: help output, basic invocation + +These tests do NOT require Veracode API credentials. +""" + +import shutil +import subprocess +import pytest +from pathlib import Path + +RELEASE_DIR = Path(__file__).parent.parent.parent / "Scripts" / "Release" +DEV_BASH_DIR = Path(__file__).parent.parent.parent / "Scripts" / "Dev" / "bash_scripts" +XML_API_DIR = Path(__file__).parent.parent.parent / "xml_api_calls" + +_RELEASE_SCRIPTS_CANDIDATES = [ + RELEASE_DIR / "veracode-installer.sh", + RELEASE_DIR / "DAST-ls-v2.sh", + RELEASE_DIR / "DAST-ls.sh", + RELEASE_DIR / "DAST-rescan.sh", + RELEASE_DIR / "SearchBuildByName.sh", + RELEASE_DIR / "vdb-purl-lte.sh", +] + +_DEV_BASH_SCRIPTS_CANDIDATES = [ + DEV_BASH_DIR / "veracode-installer.sh", + DEV_BASH_DIR / "UploadExtended.sh", + DEV_BASH_DIR / "SAST-promoteSandbox.sh", + DEV_BASH_DIR / "SCA-Library-ProjectSearch.sh", + DEV_BASH_DIR / "pipelinescan-sandboxscan-filesizecheck.sh", +] + +# Only include scripts that actually exist on disk so ids always match values +RELEASE_SCRIPTS = [s for s in _RELEASE_SCRIPTS_CANDIDATES if s.exists()] +DEV_BASH_SCRIPTS = [s for s in _DEV_BASH_SCRIPTS_CANDIDATES if s.exists()] +ALL_SCRIPTS = RELEASE_SCRIPTS + DEV_BASH_SCRIPTS + + +# --- Bash syntax validation --- + +class TestBashSyntax: + @pytest.mark.parametrize( + "script", + [s for s in RELEASE_SCRIPTS if s.exists()], + ids=[s.name for s in RELEASE_SCRIPTS], + ) + def test_release_script_syntax(self, script): + """All Release scripts should pass bash -n syntax check.""" + result = subprocess.run( + ["bash", "-n", str(script)], + capture_output=True, + text=True, + ) + assert result.returncode == 0, ( + f"Syntax error in {script.name}:\n{result.stderr}" + ) + + @pytest.mark.parametrize( + "script", + [s for s in DEV_BASH_SCRIPTS if s.exists()], + ids=[s.name for s in DEV_BASH_SCRIPTS], + ) + def test_dev_bash_script_syntax(self, script): + """All Dev bash scripts should pass bash -n syntax check.""" + result = subprocess.run( + ["bash", "-n", str(script)], + capture_output=True, + text=True, + ) + assert result.returncode == 0, ( + f"Syntax error in {script.name}:\n{result.stderr}" + ) + + +# --- ShellCheck linting --- + +@pytest.mark.skipif( + not shutil.which("shellcheck"), + reason="shellcheck not installed — install with: sudo apt-get install shellcheck", +) +class TestShellCheck: + @pytest.mark.parametrize( + "script", + [s for s in RELEASE_SCRIPTS if s.exists()], + ids=[s.name for s in RELEASE_SCRIPTS], + ) + def test_shellcheck_release_script(self, script): + """Release scripts should pass shellcheck at warning severity.""" + result = subprocess.run( + ["shellcheck", "--severity=warning", str(script)], + capture_output=True, + text=True, + ) + assert result.returncode == 0, ( + f"shellcheck issues in {script.name}:\n{result.stdout}" + ) + + @pytest.mark.parametrize( + "script", + [s for s in DEV_BASH_SCRIPTS if s.exists()], + ids=[s.name for s in DEV_BASH_SCRIPTS], + ) + def test_shellcheck_dev_script(self, script): + """Dev bash scripts should pass shellcheck at warning severity.""" + result = subprocess.run( + ["shellcheck", "--severity=warning", str(script)], + capture_output=True, + text=True, + ) + assert result.returncode == 0, ( + f"shellcheck issues in {script.name}:\n{result.stdout}" + ) + + +# --- Functional smoke tests --- + +class TestInstallerScript: + def test_installer_help_prints_usage(self): + """Installer script should print help text when invoked with unknown args.""" + script = RELEASE_DIR / "veracode-installer.sh" + if not script.exists(): + pytest.skip("veracode-installer.sh not found in Release") + result = subprocess.run( + ["bash", str(script), "--help"], + capture_output=True, + text=True, + ) + # --help hits the *) case which calls help() and exits 1 + combined = result.stdout + result.stderr + assert "Veracode" in combined or "install" in combined.lower(), ( + f"Expected help output, got:\n{combined}" + ) + + def test_installer_help_lists_options(self): + """Help output should list known installer flags.""" + script = RELEASE_DIR / "veracode-installer.sh" + if not script.exists(): + pytest.skip("veracode-installer.sh not found in Release") + result = subprocess.run( + ["bash", str(script), "--help"], + capture_output=True, + text=True, + ) + combined = result.stdout + result.stderr + assert "--install-sca-ci" in combined or "--install-sca-cli" in combined + + +class TestSearchBuildByName: + def test_script_has_valid_syntax(self): + script = RELEASE_DIR / "SearchBuildByName.sh" + if not script.exists(): + pytest.skip("SearchBuildByName.sh not found") + result = subprocess.run( + ["bash", "-n", str(script)], + capture_output=True, + text=True, + ) + assert result.returncode == 0, f"Syntax error:\n{result.stderr}" + + def test_script_exists_and_is_readable(self): + script = RELEASE_DIR / "SearchBuildByName.sh" + if not script.exists(): + pytest.skip("SearchBuildByName.sh not found") + assert script.stat().st_size > 0 + + +class TestDASTRescan: + def test_script_has_valid_syntax(self): + script = RELEASE_DIR / "DAST-rescan.sh" + if not script.exists(): + pytest.skip("DAST-rescan.sh not found") + result = subprocess.run( + ["bash", "-n", str(script)], + capture_output=True, + text=True, + ) + assert result.returncode == 0, f"Syntax error:\n{result.stderr}" + + +class TestDASTLsV2: + def test_script_has_valid_syntax(self): + script = RELEASE_DIR / "DAST-ls-v2.sh" + if not script.exists(): + pytest.skip("DAST-ls-v2.sh not found") + result = subprocess.run( + ["bash", "-n", str(script)], + capture_output=True, + text=True, + ) + assert result.returncode == 0, f"Syntax error:\n{result.stderr}" + + def test_script_is_nonempty(self): + script = RELEASE_DIR / "DAST-ls-v2.sh" + if not script.exists(): + pytest.skip("DAST-ls-v2.sh not found") + assert script.stat().st_size > 100 + + +# --- XML API scripts --- + +class TestXMLAPIScripts: + XML_SCRIPTS = list(XML_API_DIR.glob("*.sh")) if XML_API_DIR.exists() else [] + + @pytest.mark.parametrize( + "script", + XML_SCRIPTS, + ids=[s.name for s in XML_SCRIPTS], + ) + def test_xml_api_script_syntax(self, script): + """XML API helper scripts should have valid bash syntax.""" + result = subprocess.run( + ["bash", "-n", str(script)], + capture_output=True, + text=True, + ) + assert result.returncode == 0, ( + f"Syntax error in {script.name}:\n{result.stderr}" + ) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_csv_parsing.py b/tests/unit/test_csv_parsing.py new file mode 100644 index 0000000..1434a25 --- /dev/null +++ b/tests/unit/test_csv_parsing.py @@ -0,0 +1,177 @@ +""" +Unit tests for CSV-to-JSON parsing logic. + +Tests the allowlist and blacklist CSV parsing behavior that feeds into +DASTWebAppRequest-std.py and BlackList-std.py, verified against fixture files +and controlled temporary CSVs. +""" + +import csv +import json +import pytest +from pathlib import Path + +FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" + +VALID_RESTRICTION_TYPES = { + "NONE", + "FILE", + "FOLDER_ONLY", + "DIRECTORY_AND_SUBDIRECTORY", +} + + +# --- Helpers mirroring script CSV parsing logic --- + +def parse_allowlist_csv(csv_path: str) -> dict: + """ + Mirror of allowlistConfigCSVtoJSON() from DASTWebAppRequest-std.py. + Returns {"allowed_hosts": [...]} or raises on error. + """ + allowed_hosts = [] + with open(csv_path, "r") as f: + reader = csv.DictReader(f) + for row in reader: + allowed_hosts.append({ + "directory_restriction_type": row["directory_restriction_type"], + "http_and_https": str(row["http_and_https"]).strip().lower(), + "url": row["url"].strip(), + }) + return {"allowed_hosts": allowed_hosts} + + +def parse_blacklist_csv(csv_path: str) -> list: + """ + Mirror of blacklistConfigCSVtoJSON() from BlackList-std.py. + Returns a list of blacklist entry dicts. + """ + entries = [] + with open(csv_path, "r") as f: + reader = csv.DictReader(f) + for row in reader: + entries.append({ + "directory_restriction_type": row["directory_restriction_type"], + "http_and_https": str(row["http_and_https"]).strip().lower(), + "url": row["url"].strip(), + }) + return entries + + +# --- Allowlist fixture tests --- + +class TestAllowlistCSVFixture: + def test_fixture_loads_without_error(self): + result = parse_allowlist_csv(FIXTURES_DIR / "allowlist.csv") + assert result is not None + + def test_result_has_allowed_hosts_key(self): + result = parse_allowlist_csv(FIXTURES_DIR / "allowlist.csv") + assert "allowed_hosts" in result + + def test_fixture_has_multiple_entries(self): + result = parse_allowlist_csv(FIXTURES_DIR / "allowlist.csv") + assert len(result["allowed_hosts"]) >= 2 + + def test_each_entry_has_required_fields(self): + result = parse_allowlist_csv(FIXTURES_DIR / "allowlist.csv") + for host in result["allowed_hosts"]: + assert "directory_restriction_type" in host + assert "http_and_https" in host + assert "url" in host + + def test_urls_are_nonempty(self): + result = parse_allowlist_csv(FIXTURES_DIR / "allowlist.csv") + for host in result["allowed_hosts"]: + assert host["url"] != "" + + def test_http_and_https_is_boolean_string(self): + result = parse_allowlist_csv(FIXTURES_DIR / "allowlist.csv") + for host in result["allowed_hosts"]: + assert host["http_and_https"].lower() in ("true", "false") + + def test_restriction_types_are_valid(self): + result = parse_allowlist_csv(FIXTURES_DIR / "allowlist.csv") + for host in result["allowed_hosts"]: + assert host["directory_restriction_type"] in VALID_RESTRICTION_TYPES + + +# --- Blacklist fixture tests --- + +class TestBlacklistCSVFixture: + def test_fixture_loads_without_error(self): + result = parse_blacklist_csv(FIXTURES_DIR / "blacklist.csv") + assert result is not None + + def test_fixture_has_entries(self): + result = parse_blacklist_csv(FIXTURES_DIR / "blacklist.csv") + assert len(result) >= 1 + + def test_each_entry_has_required_fields(self): + result = parse_blacklist_csv(FIXTURES_DIR / "blacklist.csv") + for entry in result: + assert "directory_restriction_type" in entry + assert "http_and_https" in entry + assert "url" in entry + + def test_restriction_types_are_valid(self): + result = parse_blacklist_csv(FIXTURES_DIR / "blacklist.csv") + for entry in result: + assert entry["directory_restriction_type"] in VALID_RESTRICTION_TYPES + + def test_http_and_https_is_boolean_string(self): + result = parse_blacklist_csv(FIXTURES_DIR / "blacklist.csv") + for entry in result: + assert entry["http_and_https"].lower() in ("true", "false") + + +# --- Edge case tests with tmp files --- + +class TestCSVEdgeCases: + def test_empty_allowlist_returns_empty_list(self, tmp_path): + csv_file = tmp_path / "allowlist.csv" + csv_file.write_text("directory_restriction_type,http_and_https,url\n") + result = parse_allowlist_csv(csv_file) + assert result["allowed_hosts"] == [] + + def test_empty_blacklist_returns_empty_list(self, tmp_path): + csv_file = tmp_path / "blacklist.csv" + csv_file.write_text("directory_restriction_type,http_and_https,url\n") + result = parse_blacklist_csv(csv_file) + assert result == [] + + def test_single_allowlist_entry(self, tmp_path): + csv_file = tmp_path / "allowlist.csv" + csv_file.write_text( + "directory_restriction_type,http_and_https,url\n" + "NONE,TRUE,https://single.example.com\n" + ) + result = parse_allowlist_csv(csv_file) + assert len(result["allowed_hosts"]) == 1 + assert result["allowed_hosts"][0]["url"] == "https://single.example.com" + + def test_multiple_blacklist_entries(self, tmp_path): + csv_file = tmp_path / "blacklist.csv" + csv_file.write_text( + "directory_restriction_type,http_and_https,url\n" + "NONE,TRUE,https://blocked1.example.com\n" + "FILE,FALSE,https://blocked2.example.com\n" + "DIRECTORY_AND_SUBDIRECTORY,TRUE,https://blocked3.example.com\n" + ) + result = parse_blacklist_csv(csv_file) + assert len(result) == 3 + + def test_missing_file_raises_error(self, tmp_path): + with pytest.raises((FileNotFoundError, OSError)): + parse_allowlist_csv(tmp_path / "nonexistent.csv") + + def test_result_serializes_to_json(self): + result = parse_allowlist_csv(FIXTURES_DIR / "allowlist.csv") + serialized = json.dumps(result) + assert isinstance(serialized, str) + reparsed = json.loads(serialized) + assert reparsed["allowed_hosts"] == result["allowed_hosts"] + + def test_glblacklist_fixture_loads(self): + result = parse_blacklist_csv(FIXTURES_DIR / "glblacklist.csv") + assert isinstance(result, list) + assert len(result) >= 1 diff --git a/tests/unit/test_email_validation.py b/tests/unit/test_email_validation.py new file mode 100644 index 0000000..75114a2 --- /dev/null +++ b/tests/unit/test_email_validation.py @@ -0,0 +1,69 @@ +""" +Unit tests for email validation logic used in DASTWebAppRequest-std.py. + +The is_valid_email() function uses the pattern r'^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$'. +These tests verify that behavior without importing the script (which has +module-level side effects including stdin prompts and file I/O). +""" + +import re +import pytest + +# Pattern mirrored from Scripts/Release/DASTWebAppRequest-std.py +EMAIL_PATTERN = r'^[\w\.-]+@[\w\.-]+\.\w+$' + + +def is_valid_email(email: str) -> bool: + """Mirror of is_valid_email() from DASTWebAppRequest-std.py.""" + return bool(re.match(EMAIL_PATTERN, email)) + + +class TestValidEmails: + def test_simple_email(self): + assert is_valid_email("user@example.com") + + def test_subdomain_email(self): + assert is_valid_email("user@mail.example.com") + + def test_dot_in_local_part(self): + assert is_valid_email("first.last@example.com") + + def test_hyphen_in_domain(self): + assert is_valid_email("user@my-company.com") + + def test_numeric_local_part(self): + assert is_valid_email("user123@example.com") + + def test_underscore_in_local(self): + assert is_valid_email("user_name@example.com") + + def test_multi_level_tld(self): + assert is_valid_email("user@example.co.uk") + + +class TestInvalidEmails: + def test_no_at_sign(self): + assert not is_valid_email("userexample.com") + + def test_no_domain_after_at(self): + assert not is_valid_email("user@") + + def test_no_tld(self): + assert not is_valid_email("user@example") + + def test_empty_string(self): + assert not is_valid_email("") + + def test_only_at_sign(self): + assert not is_valid_email("@") + + def test_plus_sign_not_supported(self): + # The script's pattern uses [\w\.-] which does NOT include + + # This documents that the pattern rejects plus-addressed emails + assert not is_valid_email("user+tag@example.com") + + def test_spaces_rejected(self): + assert not is_valid_email("user @example.com") + + def test_double_at(self): + assert not is_valid_email("user@@example.com") diff --git a/tests/unit/test_schedule_helpers.py b/tests/unit/test_schedule_helpers.py new file mode 100644 index 0000000..ee6bb46 --- /dev/null +++ b/tests/unit/test_schedule_helpers.py @@ -0,0 +1,137 @@ +""" +Unit tests for scan scheduling helper functions. + +These mirror the schedule logic from Scripts/Release/DASTWebAppRequest-std.py, +tested in isolation to avoid module-level side effects. +""" + +import pytest + + +# --- Mirrors of schedule helpers from DASTWebAppRequest-std.py --- + +def _is_true(value, var_true: bool = False): + if str(value).casefold() == "true": + return True if var_true else "true" + return False if var_true else "false" + + +def schedule_now(now_b: str = _is_true(False), days: int = 1) -> dict: + """Mirror of scheduleNow() from DASTWebAppRequest-std.py.""" + schedule = {"schedule": { + "now": now_b, + "duration": { + "length": str(days), + "unit": "DAY" + } + }} + if now_b == _is_true(True): + schedule["schedule"]["scheduled"] = True + return schedule + + +def schedule_scan(start_now: bool = False, length: int = 1, unit: str = "DAY", + recurring: bool = False, recurrence_type: str = "WEEKLY", + schedule_end_after: int = 2, recurrence_interval: int = 1, + day_of_week: str = "FRIDAY") -> dict: + """Mirror of scheduleScan() from DASTWebAppRequest-std.py.""" + schedule = {"schedule": { + "duration": {"length": length, "unit": unit} + }} + if recurring: + schedule["schedule"]["scan_recurrence_schedule"] = { + "recurrence_type": recurrence_type, + "schedule_end_after": schedule_end_after, + "recurrence_interval": recurrence_interval, + "day_of_week": day_of_week + } + if start_now: + schedule["schedule"].update({"scheduled": True, "now": True}) + return schedule + + +# --- Tests --- + +class TestIsTrue: + def test_true_string_returns_true_string(self): + assert _is_true("true") == "true" + + def test_false_string_returns_false_string(self): + assert _is_true("false") == "false" + + def test_true_bool_with_var_true_returns_python_true(self): + assert _is_true(True, var_true=True) is True + + def test_false_bool_with_var_true_returns_python_false(self): + assert _is_true(False, var_true=True) is False + + def test_case_insensitive_TRUE(self): + assert _is_true("TRUE") == "true" + + def test_case_insensitive_True(self): + assert _is_true("True") == "true" + + +class TestScheduleNow: + def test_returns_dict(self): + result = schedule_now() + assert isinstance(result, dict) + + def test_has_schedule_key(self): + result = schedule_now() + assert "schedule" in result + + def test_default_now_is_false_string(self): + result = schedule_now() + assert result["schedule"]["now"] == "false" + + def test_now_true_sets_scheduled_flag(self): + result = schedule_now(now_b="true") + assert result["schedule"].get("scheduled") is True + + def test_duration_unit_is_day(self): + result = schedule_now() + assert result["schedule"]["duration"]["unit"] == "DAY" + + def test_custom_days_reflected_as_string(self): + result = schedule_now(days=5) + assert result["schedule"]["duration"]["length"] == "5" + + def test_one_day_default(self): + result = schedule_now() + assert result["schedule"]["duration"]["length"] == "1" + + +class TestScheduleScan: + def test_returns_dict(self): + result = schedule_scan() + assert isinstance(result, dict) + + def test_has_schedule_key(self): + result = schedule_scan() + assert "schedule" in result + + def test_default_unit_is_day(self): + result = schedule_scan() + assert result["schedule"]["duration"]["unit"] == "DAY" + + def test_start_now_sets_flags(self): + result = schedule_scan(start_now=True) + assert result["schedule"]["now"] is True + assert result["schedule"]["scheduled"] is True + + def test_recurring_adds_recurrence_block(self): + result = schedule_scan(recurring=True) + assert "scan_recurrence_schedule" in result["schedule"] + + def test_recurring_day_of_week(self): + result = schedule_scan(recurring=True, day_of_week="MONDAY") + assert result["schedule"]["scan_recurrence_schedule"]["day_of_week"] == "MONDAY" + + def test_non_recurring_has_no_recurrence_block(self): + result = schedule_scan(recurring=False) + assert "scan_recurrence_schedule" not in result["schedule"] + + def test_custom_length(self): + result = schedule_scan(length=7) + assert result["schedule"]["duration"]["length"] == 7