Scanners
One include line (GitLab) or one workflow file (GitHub, Gitea, Forgejo, Bitbucket) runs every scanner in the MopokeHQ scanner image and sends the results to MopokeHQ. Setup is in the guide for your platform: GitLab, GitHub, Gitea and Forgejo, any other CI.
| Scanner | Tool | What it finds | MOPOKE_SCANNERS name |
|---|---|---|---|
| Dependencies | syft | The SBOM: every package and version your app uses, which advisories are matched against | sbom |
| Code | semgrep, with MopokeHQ’s own rules | SQL and command injection, unsafe deserialisation, eval, cross-site scripting, TLS checks turned off, weak password hashing. Rules for PHP, JavaScript and TypeScript, Python and Go. |
sast |
| Secrets | gitleaks | Committed credentials, in the working tree and in a merge request’s own commits | secrets |
| Infrastructure as code | trivy | Problems in Dockerfiles, Kubernetes manifests, Helm charts, Terraform and CloudFormation | iac |
| Licences | the SBOM’s licence data | AGPL, SSPL, BUSL and packages with no licence, by policy | licence |
| Container images | syft and trivy | An image your pipeline built: its packages, and its operating system packages’ known holes | container |
Checks of your live websites from the outside are separate: see External scanning.
What is matched
Section titled “What is matched”The SBOM is matched against security advisories continuously, not only when you scan: a new advisory is checked against every SBOM MopokeHQ holds, so last month’s build is matched against tonight’s advisory without a rescan.
Matching covers:
- npm packages (from
package-lock.json,pnpm-lock.yaml,yarn.lockand installednode_modules); - Composer packages (Packagist, from
composer.lock); - PHP and Node.js runtime security releases, for the runtime versions MopokeHQ knows you run.
Packages from other ecosystems (Go, Python, Ruby, Rust, Java and more) are listed in the SBOM but not matched yet. Operating system packages inside a container image (apk, deb, rpm) are reported by the container scanner as findings instead.
How each match is ranked is on Priorities and deadlines.
Variables
Section titled “Variables”Set these as CI/CD variables (GitLab) or Actions variables (GitHub, Gitea, Forgejo).
| Variable | Default | |
|---|---|---|
MOPOKE_SCANNERS |
all but container |
A list (sbom,secrets), all, or removals (-licence,-iac). |
MOPOKE_GATE |
unset | enforce: a merge request that fails the merge gate fails its pipeline (exit code 3). The project’s gate must also be set to Enforce in MopokeHQ. |
MOPOKE_IMAGE |
unset | An image reference to scan; turns on container. |
MOPOKE_REGISTRY_USER, MOPOKE_REGISTRY_PASSWORD |
On GitLab, the job’s own login for images in the project’s registry | A registry login for MOPOKE_IMAGE. Passed to the scanners in their environment, never on a command line. |
MOPOKE_LICENCE_POLICY |
built in | A licence policy file. A repository can also commit .mopoke/licence-policy.json. |
MOPOKE_SEMGREP_CONFIG |
unset | Extra semgrep configs, comma separated. A repository’s own .semgrep.yml or .semgrep/ is picked up on its own. |
MOPOKE_TRUSTED_REGISTRIES |
On GitLab, the project’s own registry | More image registry hosts, comma separated, whose images don’t count as from an untrusted registry. |
MOPOKE_SCAN_TIMEOUT |
15m |
Per tool. |
MOPOKE_TRIVY_CACHE |
a temporary directory | Where trivy keeps its vulnerability database for image scans. Cache it between jobs to save the download. |
gitleaks reads a repository’s .gitleaks.toml and .gitleaksignore as usual.
Scan a built image
Section titled “Scan a built image”The main scan works on the checked-out code and starts with the pipeline, before any image exists. Images are scanned by a second job that runs after the build has pushed the image, with MOPOKE_IMAGE set to it. On GitLab:
mopoke-image: extends: .mopoke-image needs: [build] variables: MOPOKE_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHAElsewhere, run mopoke scan . with MOPOKE_SCANNERS=container and MOPOKE_IMAGE after the image is pushed (an example for GitHub is on Set up GitHub).
The image’s packages are matched like the repository’s own, on the same repository, as likely live when the image comes from the default branch. A new tag of the same image replaces the last one rather than adding to it. Its operating system packages’ known holes are reported as container findings.
PHP measured from the image
Section titled “PHP measured from the image”An image scan is the one place MopokeHQ sees the PHP that actually shipped, rather than the version a Dockerfile asked for. It reads the version from the php, php-fpm and php-cli binaries of the official images and other source builds; Sury and Remi packages count too. PHP from a distribution’s own archive (Debian, Ubuntu, Alpine, RHEL) is left to trivy, because those backport fixes without changing the version number.
Only a PHP version measured this way confirms a PHP fix on its own. For it to speak for an environment, the deploy report names the image it shipped: mopoke deploy --image <ref>, or MOPOKE_IMAGE on the deploy job (see Set up GitLab). Otherwise the repository’s PHP comes from --runtime php=X.Y.Z, or a Dockerfile FROM php:X.Y.Z, or for a floating tag such as php:8.3-fpm, the newest 8.3 release, assumed. Those open problems but never close them on their own: a person confirms the fix.
Exit codes
Section titled “Exit codes”| Code | Meaning | In the templates |
|---|---|---|
| 0 | Everything ran and was sent | |
| 1 | A scanner or an upload failed; the rest was sent | Warning |
| 2 | Usage: no token, no project | Warning |
| 3 | The merge gate failed, with MOPOKE_GATE=enforce |
Fails the pipeline |
How findings work
Section titled “How findings work”Code, secrets, infrastructure, licence and container results are findings, listed on the Issues page by kind, most serious first.
- Per repository and tool. Findings belong to a repository (or a monorepo path) and are tracked per kind and tool, and for images per image repository.
- Baseline. The first default-branch scan with a tool is the baseline: what it finds is existing work, listed and tagged Baseline, never counted as new.
- Default branch. Each later default-branch scan opens what is new (or came back) and marks fixed what it no longer reports.
- Merge requests. A merge request scan changes nothing on the default branch. Its results are kept so MopokeHQ can say what the merge request adds; a result is new unless the default branch has it open, someone dismissed it, or the default branch fixed it since.
- Stable identity. A finding keeps its identity when lines move: it is fingerprinted by its rule, file and the flagged text, not its line number. Infrastructure checks use the rule and file only, so bumping a base image is not a new finding.
- Grouped. Infrastructure findings in the same file are one row (“Runs as root and 7 more checks”), which unfolds to each check: one container without a security context trips a dozen checks, and fixing it is one change.
- Severity. From the rule’s own score where it has one (9 and up critical, 7 high, 4 medium), then its tags, then its level.
- Dismiss and reopen. Dismiss a finding with a reason: not a real issue, test or example code, accepted risk with a note, or for secrets, rotated and revoked. Owners and admins dismiss findings; with teams, a Lead can too in the projects they lead, except a critical finding, which always needs an owner or admin. A scan that finds a dismissed finding again leaves it dismissed. Every dismissal and reopening is recorded in the evidence log.
Left out before upload
Section titled “Left out before upload”Some results never need a person, so the CLI drops them on the runner and the job log says how many:
- code results in test code:
test/,tests/,__tests__/,__mocks__/,testdata/,*.test.*,*.spec.*, plusvendor/,dist/,build/and*.min.js; - infrastructure results in files that only describe a developer workspace (
.gitpod*,.devcontainer/); - infrastructure style checks, and checks Kubernetes makes moot (no
HEALTHCHECK,RUN cd, namespace quotas); - “untrusted registry” when every image a manifest points at comes from your own registry (
MOPOKE_TRUSTED_REGISTRIES).
What leaves the runner
Section titled “What leaves the runner”No source code. The CLI rewrites each tool’s output to a small subset: rule, file, line, severity and message, with no code snippets, code flows or surrounding source, and only the rules the results use. A secret’s value never leaves the runner, and secrets keep no message on the server either.
Secrets
Section titled “Secrets”A secret is leaked the moment it is pushed, on any branch, so a merge request scan records it as a finding straight away.
A real secret is critical. It goes out at once as a P1 alert, one email per pipeline, with a link to the finding and the steps in Secret rotation. Anything else the secrets scanner finds is high: listed for someone to check, without the alert.
The CLI judges each secret on the runner, from the file and the flagged line, and sends only labels: the kind of file (an env file that is not an example, a Kubernetes Secret) and the value’s shape (a provider’s key format, random, an ID, a placeholder, a test value). The value is read there and never kept or sent. A secret is critical when:
- a provider-specific rule found it, or
- the generic rule found a value in a provider’s key format, or
- the generic rule found a random value in a deployable env file or a Kubernetes Secret;
and it is not a placeholder or test value, not in a test, fixture, example or documentation path, and not in google-services.json or GoogleService-Info.plist (whose keys ship in every build of a mobile app and are public by design).
A secret found by a broad rule or in test code is high: its page says it may be a test value or a false match, and offers Not a real secret before Mark rotated.
Deleting a secret from the code does not fix it. It stays in history and in every clone. Secrets are never marked fixed by a scan: they stay open until someone marks them rotated, or dismisses them as not a real secret.
The working tree is always scanned. In a merge request pipeline the merge request’s own commits are scanned too, so a secret added and removed within it is still caught. For a monorepo path, only the working tree is scanned.
Who added it, and Lilly’s note
Section titled “Who added it, and Lilly’s note”For a secret MopokeHQ has no context for yet, the scan job also runs git blame on the line and sends the commit, its author, date and subject, the variable’s name (STRIPE_SECRET_KEY in STRIPE_SECRET_KEY=...), and up to 10 tracked files that mention that name. Never the value. The finding then shows Added by, linked to the commit, and Variable, with the files that use it.
Lilly writes a short note on each secret from that context: what the credential most likely is, what someone holding it could do, how exposed it is, and the first thing to do. She is never sent the value, nor who committed it. With AI switched off for your organisation, nothing is sent and findings show no note.
Licences
Section titled “Licences”Many manifests record no licences: go.mod, requirements.txt, Pipfile.lock, poetry.lock, Podfile.lock, Cargo.lock and Gemfile.lock never do, and npm’s lockfiles record only what the registry declared. Without installed dependencies beside them there is nothing to check, so the job log says so (“go.mod records no licences, so 24 packages weren’t checked”) instead of reporting every package as unlicensed. Packages that really have no licence are one finding per manifest.
The policy is a JSON file. Commit it as .mopoke/licence-policy.json, or point MOPOKE_LICENCE_POLICY at it:
{ "version": 1, "rules": [ { "id": "network-copyleft", "licences": ["AGPL-*"], "severity": "high", "title": "Network copyleft licence", "why": "..." }, { "id": "unknown", "licences": ["unknown", "UNLICENSED", "NOASSERTION", "NONE"], "severity": "low", "title": "No licence found", "why": "..." } ], "allow": ["pkg:npm/some-package"]}A trailing * matches a prefix. A package that declares several licences, or an expression with OR, is fine if any alternative is; within AND the worst licence counts. allow lists packages (as package URLs) that are fine whatever their licence.
The scanner image
Section titled “The scanner image”gitlab.boosoft.com.au:5050/mopokehq/scanner:1, public, pulled without a login. :1 follows the current release; each release is also tagged with its short commit.
- Pinned. Every tool is pinned to a release at least 14 days old and checked against a pinned SHA-256 when the image is built: syft 1.54.1, gitleaks 8.30.1, trivy 0.75.0 and semgrep 1.180.0 (only its compiled engine, so the image has no Python), on Alpine 3.23.6 pinned by digest. Licence texts are in
/usr/share/licenses. - Offline. Nothing is downloaded at scan time except trivy’s vulnerability database for image scans. semgrep runs with metrics off and local rules only; trivy uses the infrastructure checks built into it.
- MopokeHQ’s own rules. The semgrep rules are written by MopokeHQ, each with test cases.
- Mirroring. To run from your own registry, copy the image there and set
MOPOKE_SCANNER_IMAGE(GitLab CI/CD variable, or Actions variable on GitHub, Gitea and Forgejo).