diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000..48fab51 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,22 @@ +{ + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "version": "2.10.2", + "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:23ae11a86089da5f0b98a6edd603f91831802b7f2d5ef1e104e1b94a3beb546c", + "integrity": "sha256:23ae11a86089da5f0b98a6edd603f91831802b7f2d5ef1e104e1b94a3beb546c" + }, + "ghcr.io/devcontainers/features/python:1": { + "version": "1.4.2", + "resolved": "ghcr.io/devcontainers/features/python@sha256:bf021f1800543f08bf029c449a3f25341be782b620802befa1f8e6ee51cf6cf6", + "integrity": "sha256:bf021f1800543f08bf029c449a3f25341be782b620802befa1f8e6ee51cf6cf6" + }, + "ghcr.io/ministryofjustice/devcontainer-feature/container-structure-test:0": { + "version": "0.0.1", + "resolved": "ghcr.io/ministryofjustice/devcontainer-feature/container-structure-test@sha256:715c6924f74a1fda214480c9a4528f5bdfa69b8df9e099e053ddb8e27092616d", + "integrity": "sha256:715c6924f74a1fda214480c9a4528f5bdfa69b8df9e099e053ddb8e27092616d", + "dependsOn": [ + "ghcr.io/devcontainers/features/docker-in-docker:2" + ] + } + } +} \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..4a0d0bf --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +{ + "name": "observability-platform-grafana-api-key-rotator", + "image": "ghcr.io/ministryofjustice/devcontainer-base:latest", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/python:1": { + "version": "3.12", + "installTools": false + }, + "ghcr.io/ministryofjustice/devcontainer-feature/container-structure-test:0": {} + }, + "postCreateCommand": "bash .devcontainer/post-create.sh", + "customizations": { + "vscode": { + "extensions": [ + "EditorConfig.EditorConfig", + "GitHub.vscode-github-actions", + "GitHub.vscode-pull-request-github", + "ms-python.python", + "ms-python.pylint", + "ms-python.black-formatter", + "ms-python.isort", + "ms-python.flake8", + "ms-python.autopep8" + ] + } + } +} diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100755 index 0000000..e2a26e1 --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# Upgrade Pip +pip install --upgrade pip + +# Install dependencies +pip install --requirement requirements-dev.txt diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3ba6594 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# This file is autogenerated +[.devcontainer/devcontainer-lock.json] +end_of_line = unset +insert_final_newline = unset + +[*.json] +indent_style = space +indent_size = 2 + +[*.sh] +indent_style = space +indent_size = 2 + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ac066e6..007a75e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1 @@ -# Add a team or username to this file -# Example: -# * @ministryofjustice/operations-engineering +* @ministryofjustice/observability-platform diff --git a/.github/actions/setup-container-structure-test/action.yml b/.github/actions/setup-container-structure-test/action.yml new file mode 100644 index 0000000..7d001e4 --- /dev/null +++ b/.github/actions/setup-container-structure-test/action.yml @@ -0,0 +1,38 @@ +--- +name: Set Up Google Container Structure Test +description: This action installs Google's Container Structure Test tool. + +inputs: + version: + description: The version of Container Structure Test to install. + required: false + default: "latest" + +runs: + using: "composite" + steps: + - shell: bash + run: | + if [[ "$(uname -m)" == "x86_64" ]]; then + export architecture="amd64" + elif [[ "$(uname -m)" == "aarch64" ]]; then + export architecture="arm64" + else + echo "Unsupported architecture: $(uname -m)" + exit 1 + fi + + if [[ "${{ inputs.version }}" == "latest" ]]; then + export version="$(curl --silent https://api.github.com/repos/GoogleContainerTools/container-structure-test/releases/latest | jq -r '.tag_name')" + else + export version="${{ inputs.version }}" + fi + + mkdir --parents "${GITHUB_WORKSPACE}/.google-container-structure-test" + + curl --fail-with-body --location --silent "https://github.com/GoogleContainerTools/container-structure-test/releases/download/${version}/container-structure-test-linux-${architecture}" \ + --output "${GITHUB_WORKSPACE}/.google-container-structure-test/container-structure-test" + + chmod +x "${GITHUB_WORKSPACE}/.google-container-structure-test/container-structure-test" + + echo "${GITHUB_WORKSPACE}/.google-container-structure-test" >>"${GITHUB_PATH}" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 15fe7f0..3ee23ce 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,37 +1,36 @@ --- -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file - version: 2 updates: - - package-ecosystem: "bundler" - directory: "/" - schedule: - interval: "daily" - - package-ecosystem: "terraform" - directory: "/terraform" - schedule: - interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" - - package-ecosystem: "pip" + commit-message: + prefix: ":dependabot: github-actions" + include: "scope" + - package-ecosystem: "devcontainers" directory: "/" schedule: interval: "daily" - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "daily" - - package-ecosystem: "gomod" + commit-message: + prefix: ":dependabot: devcontainers" + include: "scope" + - package-ecosystem: "docker" directory: "/" schedule: interval: "daily" - - package-ecosystem: "docker" + commit-message: + prefix: ":dependabot: docker" + include: "scope" + - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" + commit-message: + prefix: ":dependabot: pip" + include: "scope" + groups: + boto: + patterns: + - "boto*" diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..c2d4e22 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,30 @@ +--- +name: Build and Test + +on: + pull_request: + branches: + - main + +permissions: {} + +jobs: + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + id: checkout + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + + - name: Set Up Container Structure Test + id: setup_container_structure_test + uses: ./.github/actions/setup-container-structure-test + + - name: Build and Test + id: build_and_test + shell: bash + run: | + make test diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..a00819e --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,44 @@ +--- +name: CodeQL Analysis + +on: + pull_request: + branches: + - main + push: + branches: + - main + +permissions: {} + +jobs: + codeql-analysis: + name: CodeQL Analysis + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: ["python"] + steps: + - name: Checkout + id: checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Initialise CodeQL + id: initialise_codeql + uses: github/codeql-action/init@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + with: + languages: ${{ matrix.language }} + + - name: CodeQL Autobuild + id: codeql_autobuild + uses: github/codeql-action/autobuild@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + + - name: CodeQL Analysis + id: codeql_analysis + uses: github/codeql-action/analyze@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + with: + category: "language:${{ matrix.language }}" diff --git a/.github/workflows/openssf-scorecard.yml b/.github/workflows/openssf-scorecard.yml new file mode 100644 index 0000000..75941e2 --- /dev/null +++ b/.github/workflows/openssf-scorecard.yml @@ -0,0 +1,47 @@ +--- +name: OpenSSF Scorecard + +on: + branch_protection_rule: + push: + branches: + - main + schedule: + - cron: "30 6 * * 1" + workflow_dispatch: + +permissions: {} + +jobs: + openssf-scorecard: + name: OpenSSF Scorecard + runs-on: ubuntu-latest + permissions: + id-token: write + security-events: write + steps: + - name: Checkout + id: checkout + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + + - name: Run Analysis + id: run_analysis + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: Upload SARIF + id: upload_sarif + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: SARIF Results + path: results.sarif + retention-days: 5 + + - name: Upload to CodeQL + id: upload_to_codeql + uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + with: + sarif_file: results.sarif diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b0e2fa7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,60 @@ +--- +name: Release + +on: + push: + tags: + - "*" + +permissions: {} + +jobs: + release: + name: Release + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + packages: write + steps: + - name: Checkout + id: checkout + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + + - name: Install cosign + id: install_cosign + uses: sigstore/cosign-installer@e1523de7571e31dbe865fd2e80c5c7c23ae71eb4 # v3.4.0 + + - name: Configure AWS Credentials + id: configure_aws_credentials + uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 + with: + aws-region: eu-west-2 + role-to-assume: arn:aws:iam::915524366300:role/modernisation-platform-oidc-cicd + + - name: Login to Amazon ECR + id: login_ecr + uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 + with: + registries: 374269020027 + + - name: Build and Push + id: build_and_push + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + push: true + tags: 374269020027.dkr.ecr.eu-west-2.amazonaws.com/observability-platform-grafana-api-key-rotator:${{ github.ref_name }} + + - name: Sign + id: sign + shell: bash + run: | + cosign sign --yes 374269020027.dkr.ecr.eu-west-2.amazonaws.com/observability-platform-grafana-api-key-rotator@${{ steps.build_and_push.outputs.digest }} + + - name: Verify + id: verify + run: | + cosign verify \ + --certificate-oidc-issuer=https://token.actions.githubusercontent.com \ + --certificate-identity=https://github.com/ministryofjustice/observability-platform-grafana-api-key-rotator/.github/workflows/release.yml@refs/tags/${{ github.ref_name }} \ + 374269020027.dkr.ecr.eu-west-2.amazonaws.com/observability-platform-grafana-api-key-rotator@${{ steps.build_and_push.outputs.digest }} diff --git a/.github/workflows/scan-image.yml b/.github/workflows/scan-image.yml new file mode 100644 index 0000000..96f7a69 --- /dev/null +++ b/.github/workflows/scan-image.yml @@ -0,0 +1,58 @@ +--- +name: Scan Image + +on: + pull_request: + branches: + - main + +permissions: {} + +jobs: + scan-image: + name: Scan Image + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Checkout + id: checkout + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + + - name: Build Image + id: build_image + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + push: false + load: true + tags: grafana-api-key-rotator + + - name: Scan Image + id: scan_image + uses: aquasecurity/trivy-action@d710430a6722f083d3b36b8339ff66b32f22ee55 # v0.19.0 + with: + image-ref: grafana-api-key-rotator + exit-code: 1 + format: sarif + output: trivy-results.sarif + severity: CRITICAL + limit-severities-for-sarif: true + + - name: Upload SARIF + if: always() + id: upload_sarif + uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + with: + sarif_file: trivy-results.sarif + + - name: Scan Image (On SARIF Scan Failure) + if: failure() && steps.scan_image.outcome == 'failure' + id: scan_image_on_failure + uses: aquasecurity/trivy-action@d710430a6722f083d3b36b8339ff66b32f22ee55 # v0.19.0 + with: + image-ref: grafana-api-key-rotator + exit-code: 1 + format: table + severity: CRITICAL + diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml new file mode 100644 index 0000000..3c8c84e --- /dev/null +++ b/.github/workflows/super-linter.yml @@ -0,0 +1,36 @@ +--- +name: Super-Linter + +on: + pull_request: + branches: + - main + types: + - edited + - opened + - reopened + - synchronize + +permissions: {} + +jobs: + super-linter: + name: Super-Linter + runs-on: ubuntu-latest + permissions: + contents: read + statuses: write + steps: + - name: Checkout + id: checkout + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + with: + fetch-depth: 0 + + - name: Run Super-Linter + id: super_linter + uses: super-linter/super-linter/slim@e0fc164bba85f4b58c6cd17ba1dfd435d01e8a06 # v6.3.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + LINTER_RULES_PATH: / + PYTHON_PYLINT_CONFIG_FILE: pyproject.toml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d26c2fc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +#checkov:skip=CKV_DOCKER_2: HEALTHCHECK not required - AWS Lambda does not support HEALTHCHECK +#checkov:skip=CKV_DOCKER_3: USER not required - A non-root user is used by AWS Lambda + +FROM public.ecr.aws/lambda/python:3.12@sha256:5f04a5e231d238774b62a4900bcf7927838811047600583272d590956bbec940 + +LABEL org.opencontainers.image.vendor="Ministry of Justice" \ + org.opencontainers.image.authors="Observability Platform (observability-platform@digital.justice.gov.uk)" \ + org.opencontainers.image.title="Grafana API Key Rotator" \ + org.opencontainers.image.description="Creates or updates an API key for Amazon Managed Grafana and uploads it to AWS Secrets Manager" \ + org.opencontainers.image.url="https://github.com/ministryofjustice/observability-platform-grafana-api-key-rotator" + +COPY --chown=nobody:nobody --chmod=0755 src/var/task/ ${LAMBDA_TASK_ROOT} + +RUN <