GitHub Actions Security and GitLab CI Security: Static Analysis for CI/CD
CI/CD YAML is code.
It decides which commands run, which secrets are present, which artifacts get published, and which deployment paths can reach production. In many repositories, the workflow file has more production authority than the application file being reviewed next to it.
That is why Skylos now scans GitHub Actions security and GitLab CI security patterns as part of danger analysis.
The goal is not to turn Skylos into a generic YAML linter. The goal is narrower: find high-signal CI/CD supply-chain risks in known workflow entry points before the pipeline runs.
Skylos now scans:
.github/workflows/*.yml.github/workflows/*.yamlaction.ymlaction.yaml.gitlab-ci.yml
You can run the same command you already use for security findings:
skylos . --danger
If you want the full local bundle:
skylos . -a
If your installed package does not include the latest GitLab CI checks yet, install from the main branch until the next release is available:
pip install "git+https://github.com/duriantaco/skylos.git"
skylos . --danger
After the release containing these checks is available, use the normal install path:
pip install --upgrade skylos
skylos . --danger
What Changed In Skylos
Skylos first added GitHub Actions workflow scanning, then routed config scanners through a registry, then added GitLab CI as a second provider.
That architecture matters.
GitHub Actions and GitLab CI look similar because both use YAML, but their security models are different:
- GitHub Actions has triggers,
GITHUB_TOKENpermissions,uses:references, reusable workflows, action metadata, GitHub contexts, andid-token: write. - GitLab CI has
include,image,services,variables,id_tokens,identity,secrets, runnertags,cache, and job-leveltimeout.
The scanner therefore records CI findings with provider-specific metadata instead of treating them as generic config issues.
GitHub Actions findings use:
{
"kind": "config",
"domain": "cicd",
"provider": "github_actions",
"type": "workflow"
}
GitLab CI findings use:
{
"kind": "config",
"domain": "cicd",
"provider": "gitlab_ci",
"type": "workflow"
}
That gives Skylos room to add Jenkins, Dockerfile, Terraform, or other config security checks later without pretending they all share the same rule model.
GitHub Actions Security Checks
GitHub Actions workflows are part of the software supply chain because they decide what runs on pull requests, what can write to the repository, and what can publish release artifacts.
Skylos checks for patterns such as:
pull_request_targetandworkflow_runworkflows that deserve privileged-trigger review- missing or broad
permissions - unpinned third-party actions and reusable workflows
actions/checkoutcredentials that stay persisted- GitHub context values injected directly into
run:blocks - self-hosted runner use in risky workflows
- unpinned container images
secrets: inheritin reusable workflows- overbroad secret access patterns
- unsafe writes to
GITHUB_ENVorGITHUB_PATH - broad GitHub App tokens
- cache-aware actions in release workflows
- OIDC token exposure to repo-controlled build or release scripts
- artifact uploads that do not fail on missing files
- JavaScript install commands that run lifecycle scripts
- privileged or release-like jobs without
timeout-minutes
One example is direct context injection:
steps:
- run: echo "${{ github.event.pull_request.title }}"
Pull request titles are user-controlled. GitHub's own security guidance warns that contexts can contain attacker-controlled data. A safer shape is to move the value into an environment variable and handle it like shell data:
steps:
- run: printf '%s\n' "$PR_TITLE"
env:
PR_TITLE: ${{ github.event.pull_request.title }}
Another example is a privileged release job that can request OIDC credentials while running repo-controlled scripts:
permissions:
contents: read
id-token: write
steps:
- run: ./scripts/build-and-publish.sh
OIDC is usually better than long-lived cloud secrets. The risk is mixing credential issuance with a broad build script in the same job. A cleaner pattern is:
- Build without cloud credentials.
- Upload a strict artifact.
- Publish from a smaller job that consumes the artifact and has only the credentials it needs.
GitLab CI Security Checks
GitLab CI has its own workflow security patterns. The first Skylos GitLab CI scanner intentionally focuses on .gitlab-ci.yml instead of scanning arbitrary YAML.
Skylos checks for:
image:orservices:values that are untagged, uselatest, or use unpinned Docker-in-Docker imagesinclude:projectentries without a full commit SHArefinclude:remoteentries withoutintegrity- secret-looking literal values in YAML
variables - untrusted merge request or ref metadata passed into
eval,sh -c,bash -c,python -c,node -e, and similar sinks - Docker-in-Docker services with TLS disabled
id_tokensoridentityjobs that run local build or release scripts- release/deploy-like jobs that restore cache
- release/deploy/OIDC-like jobs without
timeout - dynamic runner
tagsusing CI variables secrets:entries with multipleid_tokensbut no explicittoken
Here is a compact risky example:
include:
- project: group/security/pipelines
file: template.yml
image: python:latest
variables:
DEPLOY_TOKEN: plaintext-token-123
DOCKER_TLS_CERTDIR: ""
deploy:
stage: deploy
image: docker:latest
services:
- docker:dind
tags:
- "$RUNNER_TAG"
id_tokens:
VAULT_TOKEN:
aud: https://vault.example.com
cache:
paths:
- node_modules/
script:
- ./scripts/release.sh
- docker push registry.example.com/app:latest
There are several separate review points in that file:
- the external project include is not pinned to a full commit SHA
python:latestanddocker:latestare mutable referencesdocker:dindis not digest-pinnedDOCKER_TLS_CERTDIR: ""disables Docker-in-Docker TLSDEPLOY_TOKENlooks like a literal secret committed in YAML- the runner tag is dynamic
- the job can request OIDC-backed credentials and runs a repo-controlled release script
- the release-like job restores cache
- the release/OIDC job has no timeout
None of those require executing the pipeline to detect. That is why they are good candidates for static analysis.
What A Safer GitLab CI Shape Looks Like
This is not a universal template, but it moves the trust boundaries in the right direction:
include:
- project: group/security/pipelines
file: template.yml
ref: de0fac2e4500dabe0009e67214ff5f5447ce83dd
image: python@sha256:...
test:
stage: test
script:
- pytest
deploy:
stage: deploy
timeout: 15 minutes
id_tokens:
VAULT_TOKEN:
aud: https://vault.example.com
secrets:
PROD_PASSWORD:
vault: production/password@ops
token: $VAULT_TOKEN
script:
- echo "publish prebuilt artifact"
The important changes are:
- external CI code is pinned
- the image is digest-pinned
- secrets are not committed as YAML values
- token selection is explicit
- the privileged job has a timeout
- the publish job is smaller than the build job
You still need GitLab project settings, protected variables, protected environments, and runner isolation. Skylos does not claim to see those. It only flags risky repository-visible patterns.
What Static Analysis Can And Cannot Know
Static analysis is strongest when it stays honest about its inputs.
Skylos can see:
- workflow files
- action metadata
- GitLab CI YAML
- scripts referenced in repository code when scanning the full repo
- static strings, keys, commands, includes, permissions, cache settings, and timeout settings
Skylos cannot see:
- whether a GitLab variable is protected or masked
- how your self-hosted runner fleet is isolated
- whether a GitHub environment requires approval
- whether a Docker daemon is reachable from another job at runtime
- whether a cloud role trust policy is correctly scoped outside the repo
That is why the CI/CD scanner is conservative. It does not flag every string that looks unusual. It looks for high-risk combinations that are visible before merge.
Why We Did Not Add Jenkins, Docker, Or Terraform In This Same Pass
It is tempting to say "scan all config."
That would be the wrong architecture.
Jenkinsfile is Groovy plus plugin behavior. Dockerfile has image build and runtime semantics. Terraform has cloud provider semantics, resource graphs, state, modules, and policy concerns. They are not just more YAML-like files.
The registry added in Skylos lets each provider have its own scanner:
- GitHub Actions scanner for GitHub workflow semantics
- GitLab CI scanner for GitLab pipeline semantics
- future Jenkins scanner for Jenkins semantics
- future Dockerfile scanner for container build semantics
- future Terraform scanner for infrastructure semantics
That keeps findings more accurate and avoids one giant generic config bucket.
How To Use It
Run danger analysis:
skylos . --danger
Run the full local suite:
skylos . -a
Scan one GitLab CI file:
skylos .gitlab-ci.yml --danger
Scan one repository and emit JSON:
skylos . --danger --json
If you are already using Skylos in CI, the workflow findings show up in the same danger results as code security findings.
Related Skylos Pages
- GitHub Actions Security Scanner for CI/CD Supply Chain Risk
- Python Security Scanner for GitHub Actions
- How to Secure GitHub Actions for Python Repos
- AI Code Review for Security
Official References
- GitHub Actions script injection guidance: https://docs.github.com/en/actions/concepts/security/script-injections
- GitHub Actions security concepts: https://docs.github.com/en/actions/concepts/security
- GitLab CI YAML reference: https://docs.gitlab.com/ee/ci/yaml/
- GitLab CI includes: https://docs.gitlab.com/ci/yaml/includes/
- GitLab Docker-in-Docker guidance: https://docs.gitlab.com/ci/docker/using_docker_build/
The Practical Rule
If CI/CD YAML can publish packages, mint credentials, deploy production, or move artifacts into a release path, it deserves the same review discipline as application code.
Static analysis will not replace that review.
It can make sure the obvious risky edges are visible before the workflow runs.