diff --git a/.github/actionlint.yml b/.github/actionlint.yml new file mode 100644 index 00000000..80d459cf --- /dev/null +++ b/.github/actionlint.yml @@ -0,0 +1,3 @@ +self-hosted-runner: + labels: + - moj-cloud-platform \ No newline at end of file diff --git a/.github/actions/setup-zap/action.yml b/.github/actions/setup-zap/action.yml new file mode 100644 index 00000000..a9977063 --- /dev/null +++ b/.github/actions/setup-zap/action.yml @@ -0,0 +1,44 @@ +name: Setup OWASP ZAP +description: Installs the OWASP ZAP tool + +inputs: + version: + description: ZAP version, defaults to latest + default: latest + +runs: + using: composite + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 11 + + - name: Get ZAP version + id: latest + run: | + if [ "$version" = "latest" ]; then + version=$(curl -fsSL https://raw.githubusercontent.com/zaproxy/zap-admin/master/ZapVersions.xml | grep '' | sed 's/.*//;s/<\/version>.*//') + fi + echo "version=$version" | tee -a "$GITHUB_OUTPUT" + env: + version: ${{ inputs.version }} + shell: bash + + - name: Install ZAP + run: | + if [ -d "${tool_cache}/zap/${version}" ]; then + echo "Using cached ZAP $version" + else + curl -fsSL "https://github.com/zaproxy/zaproxy/releases/download/v${version}/ZAP_${version}_Linux.tar.gz" | tar -xz + mkdir -p "${tool_cache}/zap" "${tool_cache}/bin" + mv ZAP_* "${tool_cache}/zap/${version}" + ln -sf "${tool_cache}/zap/${version}/zap.sh" "${tool_cache}/bin/owasp-zap" + fi + echo "${tool_cache}/bin" >> "$GITHUB_PATH" + env: + tool_cache: ${{ runner.tool_cache }} + version: ${{ steps.latest.outputs.version }} + shell: bash + diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 00000000..4703c63d --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,36 @@ +name: Check +# Check source code before merging + +on: + push: + branches-ignore: + - main + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + statuses: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Lint changes + uses: github/super-linter/slim@v5 + env: + DEFAULT_BRANCH: main + VALIDATE_ALL_CODEBASE: false # changes only + VALIDATE_BASH: true + VALIDATE_BASH_EXEC: true + VALIDATE_DOCKERFILE_HADOLINT: true + VALIDATE_GITHUB_ACTIONS: true + VALIDATE_RUBY: true + VALIDATE_XML: true + VALIDATE_YAML: true + LINTER_RULES_PATH: / + GITHUB_ACTIONS_CONFIG_FILE: .github/actionlint.yml + GITHUB_ACTIONS_COMMAND_ARGS: -ignore=SC.+:info:.+ + GITHUB_TOKEN: ${{ github.token }} \ No newline at end of file diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..9920d015 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,114 @@ +name: Security + +on: + schedule: + - cron: "30 5 * * MON-FRI" # Every weekday at 05:30 UTC + workflow_dispatch: + +jobs: + zap: + runs-on: moj-cloud-platform + env: + CONFIG_FILE: ${{ github.workspace }}/.zap/autorun.yml + REPORT_DIR: ${{ github.workspace }}/.zap/zap-report + steps: + - uses: actions/checkout@v4 + + - name: Setup ZAP + uses: ./.github/actions/setup-zap + + - name: Setup Firefox + id: firefox + uses: browser-actions/setup-firefox@233224b712fc07910ded8c15fb95a555c86da76f # v1 + with: + firefox-version: latest-esr + + - name: Replace variables in config file + run: envsubst < "$CONFIG_FILE" > "$CONFIG_FILE.tmp" && mv "$CONFIG_FILE.tmp" "$CONFIG_FILE" && cat "$CONFIG_FILE" + env: + ZAP_USERNAME: ${{ secrets.ZAP_USERNAME }} + ZAP_PASSWORD: ${{ secrets.ZAP_PASSWORD }} + shell: bash + + - name: Run scan + run: owasp-zap -cmd -autorun "$CONFIG_FILE" -config selenium.firefoxBinary="$FIREFOX_BINARY" + env: + FIREFOX_BINARY: ${{ steps.firefox.outputs.firefox-path }} + shell: bash + + - name: Upload report + uses: actions/upload-artifact@v4 + with: + name: zap-report + path: ${{ env.REPORT_DIR }} + + - name: Publish HTML report + uses: JamesIves/github-pages-deploy-action@65b5dfd4f5bcd3a7403bbc2959c144256167464e # v4.5.0 + with: + folder: ${{ env.REPORT_DIR }} + target-folder: zap-report + + - name: Add HTML report URL to the job summary + run: echo '[🛡️ OWASP ZAP Report](https://ministryofjustice.github.io/hmpps-manage-a-supervision-ui/zap-report)' | tee -a "$GITHUB_STEP_SUMMARY" + + - name: Parse JSON report + id: json + run: | + risk_counts=$(jq -r '[.site[].alerts[]] | group_by(.riskcode) | map({ (.[0].riskcode): length }) | add' "$JSON_FILE") + echo "info=$(echo "$risk_counts" | jq '."0" // 0')" | tee -a "$GITHUB_OUTPUT" + echo "low=$(echo "$risk_counts" | jq '."1" // 0')" | tee -a "$GITHUB_OUTPUT" + echo "medium=$(echo "$risk_counts" | jq '."2" // 0')" | tee -a "$GITHUB_OUTPUT" + echo "high=$(echo "$risk_counts" | jq '."3" // 0')" | tee -a "$GITHUB_OUTPUT" + env: + JSON_FILE: ${{ env.REPORT_DIR }}/report.json + shell: bash + + - name: Send message to Slack + uses: slackapi/slack-github-action@6c661ce58804a1a20f6dc5fbee7f0381b469e001 # v1.25.0 + if: steps.json.outputs.high != '0' || steps.json.outputs.medium != '0' + with: + channel-id: probation-integration-notifications + payload: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "🛡️ *Manage a Supervision* ZAP report" + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": ">${{ steps.json.outputs.high }} high risk, ${{ steps.json.outputs.medium }} medium risk, and ${{ steps.json.outputs.low }} low risk issues were found." + } + ] + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "📈 Report" + }, + "url": "https://ministryofjustice.github.io/hmpps-manage-a-supervision-ui/zap-report" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "📝 Logs" + }, + "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } + ] + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.zap/autorun.yml b/.zap/autorun.yml new file mode 100644 index 00000000..35bd58c9 --- /dev/null +++ b/.zap/autorun.yml @@ -0,0 +1,72 @@ +--- # ZAP automation configuration file, for more details see https://www.zaproxy.org/docs/automate/automation-framework/ +env: + contexts: + - name: HMPPSAuth + urls: + - "https://manage-a-supervision-dev.hmpps.service.justice.gov.uk" + includePaths: + - "https://manage-a-supervision-dev.hmpps.service.justice.gov.uk.*" + excludePaths: + - "https://sign-in-dev.hmpps.service.justice.gov.uk.*" + authentication: + method: browser + parameters: + loginPageUrl: "https://sign-in-dev.hmpps.service.justice.gov.uk/auth/sign-in?redirect_uri=https://manage-a-supervision-dev.hmpps.service.justice.gov.uk/sign-in/callback" + loginRequestUrl: "https://sign-in-dev.hmpps.service.justice.gov.uk/auth/sign-in?redirect_uri=https://manage-a-supervision-dev.hmpps.service.justice.gov.uk/sign-in/callback" + verification: + method: "poll" + pollUrl: "https://manage-a-supervision-dev.hmpps.service.justice.gov.uk/" + pollFrequency: 1 + pollUnits: "requests" + loggedInRegex: "\\Q 200\\E" + loggedOutRegex: "\\Q 302\\E" + sessionManagement: + method: "cookie" + users: + - name: "TestUser" + credentials: + username: "AutomatedTestUser" + password: "oz5eo2OhceesheN7" + parameters: + failOnError: true + failOnWarning: false + progressToStdout: true + +jobs: + - type: spider + parameters: + context: "HMPPSAuth" + user: "TestUser" + url: "https://manage-a-supervision-dev.hmpps.service.justice.gov.uk/case/X756510" + maxDuration: 5 # minutes + - name: "Add script" + type: script + parameters: + action: "add" + type: "active" + engine: "ECMAScript : Oracle Nashorn" + name: "traverse.js" + file: "traverse.js" + - name: "List pages" + type: script + parameters: + action: "run" + type: "standalone" + engine: "" + name: "traverse.js" + - type: activeScan + parameters: + context: "HMPPSAuth" + user: "TestUser" + maxScanDurationInMins: 30 + - name: "HTML Report" + type: report + parameters: + reportDir: "zap-report" + reportFile: "index.html" + - name: "JSON Report" + type: report + parameters: + template: "traditional-json" + reportDir: "zap-report" + reportFile: "report.json" diff --git a/.zap/traverse.js b/.zap/traverse.js new file mode 100644 index 00000000..c7adcb45 --- /dev/null +++ b/.zap/traverse.js @@ -0,0 +1,11 @@ +function listChildren(node, level) { + var j + for (j = 0; j < node.getChildCount(); j++) { + print(Array(level + 1).join(' ') + node.getChildAt(j).getNodeName()) + listChildren(node.getChildAt(j), level + 1) + } +} + +root = model.getSession().getSiteTree().getRoot() + +listChildren(root, 0)