16 detectors.
Three enforcement gates.
Zero LLM cost.
LGTM Security scans GitHub Actions workflows, Dockerfiles, and IaC configs for the supply-chain and DevOps risks that cause real incidents. Pure code, no LLM tokens — every detector is deterministic, fast, and reproducible. Block merges. Block pushes. Auto-revert what slipped through.
Free: read-only access · Pro ₹399/mo: enroll + scan + alerts
Three enforcement gates
One detector engine, three places it runs. Inline at PR review time so devs see findings while reviewing. Merge- block at the GitHub Check Runs API so branch protection can refuse the merge. Runtime watchdog at push time on default branch so anything that slipped through still gets caught.
Inline PR review
Findings show up in the PR thread
When a PR includes changes to a watched file, LGTM Security runs the detectors and posts findings as inline comments anchored to the offending lines. Same UX as our regular AI review, just sourced from deterministic rules instead of LLM agents.
Trigger: pull_request opened / synchronize / reopened. Latency: ~3 seconds.
Merge-block Check Run
GitHub branch protection refuses the merge
LGTM Security writes a check_run with status: failure when any rule set to 'block' mode triggers. Branch protection rules respect the check the same way they respect any CI gate — no merge until resolved or rule is downgraded.
Per-rule mode: off / warn / block. Default mode varies by rule severity.
Runtime watchdog
Pushes that bypassed PR review get caught
A GitHub Action you install on default branch. Runs on every push, detects pushes that skipped PR review (force pushes, direct commits by privileged users), and either alerts or auto-creates a revert PR. Your choice per-repo.
Action: tarinagarwal/lgtm-security-watchdog@v1. Token-authed (mint from dashboard).
All 16 detectors, documented
Every rule lives by its ID — the same ID you see in the audit log, the same ID you override in the policy editor. Below: what each catches, why it's flagged, and a sample of what triggers it.
GitHub Actions
7 detectorscriticalSelf-hosted runner on a public repo
workflow.self-hosted-runner-on-public-repo
Self-hosted runner on a public repo
workflow.self-hosted-runner-on-public-repoWhy this is flagged
A self-hosted runner on a public repository lets any forked PR run arbitrary code on your infrastructure — well-known fork-pwn vector. Several real-world incidents have exfiltrated AWS creds via this route.
What triggers it
`runs-on: self-hosted` on a workflow triggered by `pull_request` (no `pull_request_target` workaround needed for the attacker).
criticalUntrusted PR input flowing into `run:` blocks
workflow.untrusted-input-in-run
Untrusted PR input flowing into `run:` blocks
workflow.untrusted-input-in-runWhy this is flagged
PR titles, branch names, issue comments and similar attacker-controlled strings interpolated into shell run-blocks become arbitrary command execution. The classic `${{ github.event.pull_request.title }}` injection.
What triggers it
`run: echo "PR: ${{ github.event.pull_request.title }}"` — title becomes shell.
highAction pinned to a mutable ref (branch/tag) instead of SHA
workflow.action-pinned-to-mutable-ref
Action pinned to a mutable ref (branch/tag) instead of SHA
workflow.action-pinned-to-mutable-refWhy this is flagged
An action pinned to a tag or branch can be quietly retagged or force-pushed by its maintainer (intentionally or after compromise) to a malicious version. Pin to a commit SHA so the bytes can't change underneath you.
What triggers it
`uses: actions/checkout@v3` → flag. `uses: actions/checkout@8e5e7e5...` → ok.
mediumDeprecated workflow commands (`set-output`, `save-state`)
workflow.deprecated-commands
Deprecated workflow commands (`set-output`, `save-state`)
workflow.deprecated-commandsWhy this is flagged
GitHub deprecated these because they were exploitable for log injection. Workflows that still use them will eventually break — and miss the security fix that came with the migration.
What triggers it
`echo "::set-output name=foo::$BAR"` instead of `echo "foo=$BAR" >> $GITHUB_OUTPUT`.
highMissing top-level `permissions:` block
workflow.missing-permissions-block
Missing top-level `permissions:` block
workflow.missing-permissions-blockWhy this is flagged
Without an explicit `permissions:` block, workflows inherit the repo-wide default — historically `write-all`. One compromised step can mint releases, push tags, or read every secret. Set `permissions: read-all` at the top and elevate per-job only when needed.
What triggers it
No `permissions:` at all → default-permissive token reaches every step.
critical`pull_request_target` checking out PR-controlled code
workflow.pull-request-target-checkout-pr-code
`pull_request_target` checking out PR-controlled code
workflow.pull-request-target-checkout-pr-codeWhy this is flagged
`pull_request_target` runs with the BASE repository's secrets, so checking out the head SHA hands fork code the keys to the kingdom. The pwnrequest pattern that hit several major repos.
What triggers it
`on: pull_request_target` + `uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }}`.
highSecret exfiltration via env-echo
workflow.secret-exfil-via-echo
Secret exfiltration via env-echo
workflow.secret-exfil-via-echoWhy this is flagged
`echo` or printf of a secret value into a logged step bypasses GitHub's secret masking when the secret is base64-encoded or transformed first. Pattern flagged because it's almost never legitimate.
What triggers it
`run: echo "${{ secrets.AWS_KEY }}" | base64` — base64 evades the masking pass.
Docker
5 detectorshigh`:latest` tag in `FROM`
docker.latest-tag-in-from
`:latest` tag in `FROM`
docker.latest-tag-in-fromWhy this is flagged
`:latest` is mutable — today's image is not tomorrow's image. Reproducible builds need pinned digests. The Lockfile-integrity philosophy applies to base images too.
What triggers it
`FROM node:latest` → flag. `FROM node:20-slim@sha256:...` → ok.
medium`apt-get install` without `--no-install-recommends`
docker.apt-get-without-no-install-recommends
`apt-get install` without `--no-install-recommends`
docker.apt-get-without-no-install-recommendsWhy this is flagged
Without `--no-install-recommends`, apt pulls in dozens of suggested packages that grow your image, expand the attack surface, and slow rebuilds. Image bloat is a security smell — every extra package is a potential CVE.
What triggers it
`RUN apt-get install -y curl` → flag. Add `--no-install-recommends`.
low`ADD` used where `COPY` would do
docker.add-instead-of-copy
`ADD` used where `COPY` would do
docker.add-instead-of-copyWhy this is flagged
`ADD` auto-extracts tarballs and fetches remote URLs — both have caused real incidents (tar bombs, fetched-from-mutated-URL). Use `COPY` unless you specifically need extraction; document why if you do.
What triggers it
`ADD ./local-file /app/` → use `COPY`.
high`curl | sh` piping
docker.curl-pipe-sh
`curl | sh` piping
docker.curl-pipe-shWhy this is flagged
Piping a remote script directly into a shell turns your build into whatever the maintainer's web server returns this second. Fetch first, verify, then execute.
What triggers it
`RUN curl -fsSL https://example.com/install.sh | sh` — no validation, no pinning.
mediumNo `USER` directive (running as root)
docker.running-as-root
No `USER` directive (running as root)
docker.running-as-rootWhy this is flagged
Containers running as root have full filesystem write access if escape-from-container ever happens. Set a `USER` to drop privileges; `node:slim`, `python:slim`, and most language images already ship with a non-root user available.
What triggers it
Missing `USER node` or `USER 1000` → root by default.
IaC (Terraform · Kubernetes)
4 detectorscriticalS3 bucket allowing public read
iac.s3-bucket-public
S3 bucket allowing public read
iac.s3-bucket-publicWhy this is flagged
Public S3 has been the root cause of repeated mass-PII leaks. The default should be private; opt into public via a known intentional path (CloudFront OAI, signed URLs, etc.).
What triggers it
`acl = "public-read"` or `block_public_acls = false` without justification.
criticalSecurity group `0.0.0.0/0` on sensitive ports
iac.security-group-open-to-world
Security group `0.0.0.0/0` on sensitive ports
iac.security-group-open-to-worldWhy this is flagged
Ports 22 (SSH), 3389 (RDP), 3306 (MySQL), 5432 (Postgres), 6379 (Redis), 27017 (Mongo) on 0.0.0.0/0 are how databases end up scraped by Shodan within an hour of being exposed.
What triggers it
`cidr_blocks = ["0.0.0.0/0"]` + `from_port = 22` → flag.
mediumKubernetes pod without resource limits
iac.k8s-pod-no-resource-limits
Kubernetes pod without resource limits
iac.k8s-pod-no-resource-limitsWhy this is flagged
A pod without CPU/memory limits can starve neighbors on the node. Also a noisy-neighbor security pattern — a compromised container without limits can mine crypto undetected for longer.
What triggers it
`resources: {}` or `resources` block omitted entirely.
criticalHardcoded credentials in IaC
iac.hardcoded-credentials
Hardcoded credentials in IaC
iac.hardcoded-credentialsWhy this is flagged
Secrets in Terraform state, K8s manifests, or environment blocks end up in version control. Even after rotation, the historical commit still has them — git history is forever.
What triggers it
`AWS_SECRET_ACCESS_KEY = "AKIA..."` checked into the repo.
Tune each rule per repo
Sensible defaults out of the box — but every rule is overridable per repo. A monorepo with a known self-hosted runner intentionally serving private PRs downgrades the relevant detector to warn without touching the others.
- block
Finding fails the GitHub check run. Branch protection refuses merge until resolved.
- warn
Finding posts a non-blocking PR comment. Visible to reviewers but doesn't gate merge.
- off
Finding still gets recorded in the audit log (for compliance) but no PR action.
Policy changes are versioned. Every override writes an audit entry — you can see who changed what and when.
Example policy table
workflow.self-hosted-runner-on-public-repoblockworkflow.action-pinned-to-mutable-refwarndocker.latest-tag-in-fromwarniac.s3-bucket-publicblockdocker.add-instead-of-copyoffRuntime watchdog
PR review is the first gate. Merge-block is the second. The watchdog is the third — it runs on every push to your default branch and catches the things that bypassed both: force pushes by maintainers, direct commits by privileged users, repository admins pushing without opening a PR.
Setup
Add the action to your default-branch workflow + mint a token from Dashboard → Security → API Tokens.
name: LGTM Security Watchdog
on:
push:
branches: [main]
jobs:
watchdog:
runs-on: ubuntu-latest
steps:
- uses: tarinagarwal/lgtm-security-watchdog@v1
with:
token: ${{ secrets.LGTM_WATCHDOG }}What it does
- ·Detects pushes that didn't go through a PR (force push, direct commit, admin bypass)
- ·Runs the 16 detectors against the push diff
- ·On finding: either Slack/email alert OR auto-creates a revert PR
- ·Token-scoped: write-only to LGTM, no GitHub-write access via this token
The watchdog Action is MIT-licensed and lives on GitHub. You can read the code and verify there's nothing weird before installing.
Immutable record of every finding
Every scan produces SecurityAuditLog entries — finding + file + line + ruleId + scanId + timestamp. Past findings can be marked resolved, false_positive, or won't fix with a note — but the original entry is never deleted.
The audit log survives repo disconnects, account deletions (anonymized but retained), and rule policy changes. If a regulator asks "was this CVE flagged in our pipeline?" — the answer is in the log, provable.
- Per-finding resolution status + free-text note
- Filter by rule ID, severity, PR number, or free-text
- False-positive rate computed per rule (for tuning policy)
- Export available via API for SIEM ingestion
Example audit entries
workflow.self-hosted-runner-on-public-repo.github/workflows/build.yml · L12
docker.latest-tag-in-fromapps/api/Dockerfile · L3
iac.security-group-open-to-worldinfra/sg-db.tf · L18
workflow.action-pinned-to-mutable-ref.github/workflows/deploy.yml · L24
Why these rules don't use LLMs
CI/CD security findings need to be reproducible, auditable, and cheap. Three things LLM scanners struggle with.
Deterministic
Same input, same output. Run the scan twice on the same diff and get the same findings. Tribunals like that. Auditors like that.
Zero LLM cost
Pure code paths — no token spend per scan. Scanning the same repo 1,000 times costs the same as scanning it once. Free tier ships with full scanner power.
No false alarms from hallucination
Rules trigger on AST-level pattern matches, not LLM judgement. False positives come from genuinely ambiguous cases, not the model getting creative.
Compliance angles
LGTM Security maps to several public security frameworks. Useful when your compliance team asks "what does this tool give us toward our SOC 2 / ISO / NIST checklist?"
OWASP CI/CD Top 10
- ·CICD-SEC-04: Poisoned Pipeline Execution → workflow.untrusted-input-in-run
- ·CICD-SEC-06: Insufficient Credential Hygiene → iac.hardcoded-credentials
- ·CICD-SEC-07: Insecure System Configuration → workflow.missing-permissions-block
- ·CICD-SEC-03: Dependency Chain Abuse → workflow.action-pinned-to-mutable-ref
NIST SSDF
- ·PO.5.1 → audit log retention
- ·PS.1.1 → secret detection in IaC
- ·PW.5.1 → policy enforcement at merge time
- ·RV.1.2 → finding triage workflow
OSSF Scorecard
- ·Pinned-Dependencies → workflow.action-pinned-to-mutable-ref
- ·Token-Permissions → workflow.missing-permissions-block
- ·Dangerous-Workflow → workflow.pull-request-target-checkout-pr-code
- ·Webhook-Validation (server) → covered by our own webhook idempotency
India DPDP Act
- ·§24: Reasonable security practices → enforced via audit log
- ·Breach reporting → audit + alert path
- ·Grievance officer access → contact via /security
- ·Data localisation note: scans run in Singapore region; no PII in findings
Free users keep their data, lose write access
When a Pro user downgrades to Free, LGTM Security flips to read-only mode. Past findings, audit log, monitor list, and scan history all stay visible. Enrolling new repos, running manual scans, editing policy, or minting watchdog tokens require Pro.
Existing monitors auto-pause on downgrade — they don't get deleted. Re-upgrade and they resume automatically. No re-enrollment, no re-token-mint, no lost configuration.
FAQ
Does LGTM Security replace Snyk / Dependabot / GitHub's CodeQL?
Why deterministic rules instead of an LLM?
Does it generate false positives?
false_positive with a note and it stops blocking; the original entry stays in the audit log. Per-rule false-positive rates are computed automatically so you can see which rules need tuning for your repo.What if I need a custom detector?
What does the GitHub App need access to for Security to work?
pull_request and push events. The watchdog Action additionally needs a token you mint from the dashboard; that token has zero GitHub permissions — it only POSTs findings to LGTM.Can I run Security on a private/internal repo?
How quickly does a scan complete?
Lock down your CI/CD
16 detectors. Zero LLM cost. Three enforcement gates. ₹399/mo Pro · Free read-only access to all findings.