How do you run SuperClue scans from GitHub Actions or GitLab CI?

Create an API key in SuperClue, store it as a CI secret, and call POST /api/cicd/scan from your pipeline. A CI scan runs the basic 10-rule SAST on code you send, or a basic security-header check of a URL. It does not run the 350-module website scanner, and each scan costs 9.52 credits.

What do you need before you start?

  • A SuperClue account with the owner or admin role, which is needed to create API keys.
  • Credits: each CI/CD scan costs 9.52 credits. If a scan fails partway, the credits are refunded.
  • curl and jq on the runner. GitHub ubuntu-latest runners have both; the GitLab snippet installs them.
  • For URL scans, the target domain must already be verified in your SuperClue account.

Steps

  1. 1

    Create an API key

    Sign in as an organisation owner or admin, open the CI/CD page in the app and create a key. Keys start with sc_ and are shown once, so copy it straight away. You can hold up to 20 keys.

  2. 2

    Store the key as a CI secret

    In GitHub, add it as a repository secret named SUPERCLUE_API_KEY. In GitLab, add it as a masked CI/CD variable with the same name.

  3. 3

    Add the pipeline file

    Copy the GitHub Actions or GitLab CI snippet below into your repository. It sends your source code as text to POST /api/cicd/scan with the X-API-Key header.

  4. 4

    Push a change and read the result

    The job fails if the API call fails, if the scan does not complete, or if any finding is critical. The response also lists how many findings are new or fixed since your previous scan.

How do you check that it is working?

A working call returns "status": "completed" with a vulnerability count, the findings, and a diff summary. The scan is saved to your account with the source cicd. Errors: 401 means a missing or revoked key, 402 means not enough credits, and 502 means the scan failed and the credits were refunded.

GitHub Actions

# .github/workflows/superclue-scan.yml
name: SuperClue Security Scan
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
jobs:
  security-scan:
    runs-on: ubuntu-latest   # has curl and jq
    steps:
      - uses: actions/checkout@v4
      - name: Run SuperClue SAST scan
        env:
          SUPERCLUE_API_KEY: ${{ secrets.SUPERCLUE_API_KEY }}
        run: |
          set -eu
          find src -name '*.py' -exec cat {} + | head -n 5000 > code.txt
          jq -Rs '{type: "sast", target: ., language: "python"}' code.txt > payload.json
          curl --fail-with-body -sS -X POST "https://superclue.io/api/cicd/scan" \
            -H "X-API-Key: $SUPERCLUE_API_KEY" \
            -H "Content-Type: application/json" \
            --data-binary @payload.json -o scan_results.json
      - name: Fail on critical findings
        run: |
          set -eu
          jq -e '.status == "completed"' scan_results.json > /dev/null
          CRITICAL=$(jq '[(.vulnerabilities // [])[] | select(.severity == "critical")] | length' scan_results.json)
          echo "New: $(jq '.diff.summary.new // 0' scan_results.json)  Fixed: $(jq '.diff.summary.fixed // 0' scan_results.json)"
          if [ "$CRITICAL" -gt 0 ]; then echo "::error::$CRITICAL critical findings"; exit 1; fi

GitLab CI

# .gitlab-ci.yml
superclue-scan:
  stage: test
  image: alpine:3.20
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      set -eu
      find src -name '*.py' -exec cat {} + | head -n 5000 > code.txt
      jq -Rs '{type: "sast", target: ., language: "python"}' code.txt > payload.json
      curl --fail-with-body -sS -X POST "https://superclue.io/api/cicd/scan" \
        -H "X-API-Key: $SUPERCLUE_API_KEY" \
        -H "Content-Type: application/json" \
        --data-binary @payload.json -o scan_results.json
      jq -e '.status == "completed"' scan_results.json > /dev/null
      CRITICAL=$(jq '[(.vulnerabilities // [])[] | select(.severity == "critical")] | length' scan_results.json)
      if [ "$CRITICAL" -gt 0 ]; then echo "$CRITICAL critical findings"; exit 1; fi
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

What do the two scan types check?

  • sast: 10 pattern-based rule groups: SQL injection, cross-site scripting, hard-coded credentials, command injection, code injection, path traversal, CSRF, insecure headers, insecure deserialisation and weak cryptography.
  • dast: six security headers, the Server banner, HTTPS and the TRACE method on the URL you send.

To check a staging URL instead of code, send type dast:

curl --fail-with-body -sS -X POST "https://superclue.io/api/cicd/scan" \
  -H "X-API-Key: $SUPERCLUE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type": "dast", "target": "https://staging.example.com"}'

Request fields

FieldMeaning
typesast or dast
targetFor sast: the source code, as text. For dast: the URL to check.
languageOptional. Accepted, but the SAST rules are language-agnostic.
baseline_scan_idOptional. Scan to compare against. If omitted, your previous completed scan of the same type is used.

The snippets above work for any language: change the find pattern to collect your own files. For the full 350-module scanner, run a website scan from the app.

How do you uninstall it?

Revoke the key on the CI/CD page in the app; it stops working at once. Then delete the workflow file and the SUPERCLUE_API_KEY secret from your repository.

Need a deeper code review than the CI rules?

Code security options