Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/site-checks/pages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
/rules/GL001/
/owasp/
/performance/
/gitlab-ultimate/
/404.html
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Source of <https://glsec.github.io>, the website and rule reference for [glsec](https://github.com/glsec/glsec).

The rule pages are generated from `docs/rules.md` and `docs/rules/GLxxx.md` in the glsec repository, at the latest release tag, so the site always describes the version people can install. The OWASP page sorts the rules from that release's `glsec list --format json` into the ten OWASP CI/CD risks. The example on the home page and the example reports are the output of that release's binary on [`sample/bad-pipeline.yml`](sample/bad-pipeline.yml), in every format (the template format with the release's example `docs/templates/mr-comment.md.tmpl`), and the pipeline inventory is its `glsec inventory` of [`sample/inventory.yml`](sample/inventory.yml). The GitLab example projects (gitlab.com/glsec-io/examples) scan the same file, and the screenshots on the reports page come from them, so keep the copies in sync.
The rule pages are generated from `docs/rules.md` and `docs/rules/GLxxx.md` in the glsec repository, at the latest release tag, so the site always describes the version people can install. The OWASP page sorts the rules from that release's `glsec list --format json` into the ten OWASP CI/CD risks. The example on the home page and the example reports are the output of that release's binary on [`sample/bad-pipeline.yml`](sample/bad-pipeline.yml), in every format (the template format with the release's example `docs/templates/mr-comment.md.tmpl`), and the pipeline inventory is its `glsec inventory` of [`sample/inventory.yml`](sample/inventory.yml). The GitLab example projects (gitlab.com/glsec-io/examples) scan the same file, and the screenshots on the reports page come from them, so keep the copies in sync. The screenshots on the GitLab Ultimate page come from the test project gitlab.com/glsec-io/ultimate-lab, which only members can see; they were taken by hand on 2026-10-27 and cropped to drop the sidebar, avatars and the GitLab Duo comment.

The performance page charts `data/benchmarks.jsonl` from glsec's [`benchmarks` branch](https://github.com/glsec/glsec/tree/benchmarks), which glsec's benchmark history workflow writes for every change to `main` and once for every release; the tags mark the releases. `perfStart` in `perf.go` sets the first release it shows.

Expand Down
8 changes: 8 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@ func build(o Options) error {
return err
}

if err := write(Page{
Title: "GitLab Ultimate: vulnerability report and policies · glsec",
Description: "glsec findings in GitLab Ultimate's vulnerability report and merge request security widget, glsec made mandatory with a pipeline execution policy, and what a scan execution policy means for GL082.",
Path: "/gitlab-ultimate/",
}, "ultimate.html"); err != nil {
return err
}

overview, err := conv.render(src, "docs/rules.md", true)
if err != nil {
return err
Expand Down
33 changes: 30 additions & 3 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func read(t *testing.T, path string) string {

func TestBuild_Pages(t *testing.T) {
out := buildFixture(t)
for _, p := range []string{"index.html", "404.html", "owasp/index.html", "reports/index.html", "performance/index.html", "rules/index.html", "rules/GL001/index.html", "rules/GL002/index.html", "style.css", "favicon.ico", "favicon-32.png", "favicon-64.png", "apple-touch-icon.png", "logo.png", "sitemap.xml", "robots.txt"} {
for _, p := range []string{"index.html", "404.html", "owasp/index.html", "reports/index.html", "performance/index.html", "gitlab-ultimate/index.html", "rules/index.html", "rules/GL001/index.html", "rules/GL002/index.html", "style.css", "favicon.ico", "favicon-32.png", "favicon-64.png", "apple-touch-icon.png", "logo.png", "sitemap.xml", "robots.txt"} {
if _, err := os.Stat(filepath.Join(out, p)); err != nil {
t.Errorf("missing %s", p)
}
Expand Down Expand Up @@ -88,7 +88,7 @@ func TestBuild_HomeAndOverview(t *testing.T) {
"mise use github:glsec/glsec",
"pipx install glsec",
`<a href="https://gitlab.com/explore/catalog/glsec-io/glsec">GitLab</a>`,
"3 rules",
"4 rules",
} {
if !strings.Contains(home, want) {
t.Errorf("home page lacks %s", want)
Expand Down Expand Up @@ -184,7 +184,7 @@ func TestBuild_OWASPPage(t *testing.T) {
`id="cicd-sec-3"`,
`<a href="/rules/GL001/"><span class="rid">GL001</span> Mutable image tag</a>`,
`<a href="/rules/GL011/"><span class="rid">GL011</span>`,
"2 rules",
"3 rules",
"1 rule<",
`href="https://owasp.org/www-project-top-10-ci-cd-security-risks/CICD-SEC-10-Insufficient-Logging-And-Visibility"`,
"not in the pipeline file",
Expand Down Expand Up @@ -277,6 +277,33 @@ func TestBuild_ReportsPage(t *testing.T) {
}
}

func TestBuild_UltimatePage(t *testing.T) {
out := buildFixture(t)
page := read(t, filepath.Join(out, "gitlab-ultimate", "index.html"))
for _, want := range []string{
"<h1>GitLab Ultimate: vulnerability report and policies</h1>",
"component: gitlab.com/glsec-io/glsec/glsec@v1.0.25",
`version: "1.23.0"`,
`<a href="/rules/GL082/#scan-execution-policies">`,
`<a href="/reports/#gitlab-sast">`,
} {
if !strings.Contains(page, want) {
t.Errorf("Ultimate page lacks %s", want)
}
}
for _, shot := range []string{"vulnerability-report", "vulnerability-detail", "mr-security-widget", "mr-security-report"} {
if !strings.Contains(page, `src="/screenshots/`+shot+`.png"`) {
t.Errorf("Ultimate page lacks the %s screenshot", shot)
}
if _, err := os.Stat(filepath.Join(out, "screenshots", shot+".png")); err != nil {
t.Error(err)
}
}
if !strings.Contains(read(t, filepath.Join(out, "reports", "index.html")), `<a href="/gitlab-ultimate/">screenshots</a>`) {
t.Error("reports page doesn't link the Ultimate page")
}
}

func TestLoadReports_RejectsEmptyOutput(t *testing.T) {
dir := t.TempDir()
for _, f := range []string{"input.yml", "text.txt", "table.txt", "codeclimate.txt", "junit.txt", "sarif.txt", "gitlab-sast.txt", "template.txt"} {
Expand Down
2 changes: 1 addition & 1 deletion reports.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ var reportFormats = []Report{
}},
{Format: "gitlab-sast", Name: "GitLab SAST",
Use: "A SAST report in GitLab's security report schema 15.2.1. Each finding is one vulnerability, named after its rule, with the CWE and OWASP category as identifiers; error, warn and info become High, Medium and Low. GitLab matches a finding from one pipeline to the next by file and line, so a merge request that moves an unchanged finding shows it as fixed and as new.",
Where: "The project's vulnerability report and the merge request's security widget, on GitLab Ultimate only. Free and Premium accept the report but don't show it; use Code Quality there.",
Where: "The project's vulnerability report and the merge request's security widget, on GitLab Ultimate only (<a href=\"/gitlab-ultimate/\">screenshots</a>). Free and Premium accept the report but don't show it; use Code Quality there.",
Wiring: `glsec-sast:
image:
name: ghcr.io/glsec/glsec:{{VERSION}}
Expand Down
Binary file added static/screenshots/mr-security-report.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/screenshots/mr-security-widget.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/screenshots/vulnerability-detail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/screenshots/vulnerability-report.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 62 additions & 0 deletions templates/pages.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,68 @@ <h2>Pipeline inventory (CycloneDX) <code>glsec inventory</code></h2>
</main>
{{template "foot" .}}{{end}}

{{define "ultimate.html"}}{{template "head" .}}
<main class="wrap doc">
<h1>GitLab Ultimate: vulnerability report and policies</h1>
<p>Everything on this page needs GitLab Ultimate. It was tried out on gitlab.com on 2026-10-27, in a public test project with a pipeline file full of deliberate findings. The screenshots show GitLab Ultimate, cropped to the part that matters. GitLab only shows the vulnerability report and the security widget to project members, so the screenshots have no link. glsec is an independent project, not affiliated with or endorsed by GitLab.</p>

<h2 id="vulnerability-report">Findings in the vulnerability report</h2>
<p>With <code>--format gitlab-sast</code>, glsec writes a SAST report in GitLab's security report schema. A job uploads it as <code>artifacts:reports:sast</code> (<a href="/reports/#gitlab-sast">the job and an example report</a>), and every glsec finding on the default branch becomes a vulnerability that can be triaged: confirmed, dismissed or linked to an issue. Error findings are High, warnings Medium, info Low.</p>
<figure class="shot"><img src="/screenshots/vulnerability-report.png" alt="GitLab vulnerability report of the test project: 9 development vulnerabilities, 2 High and 7 Medium. The list shows GL001 Mutable image tag and GL011 Download-and-execute pattern in script as High, GL028 artifacts untracked without path restriction and GL080 Sensitive job with no pipeline-source guard as Medium, each with its line in bad/.gitlab-ci.yml, report type SAST, scanner Glsec" width="1143" height="940" loading="lazy"><figcaption>The vulnerability report of the test project's default branch. The screenshot is from before glsec 1.25.0, which also lists the CWE as an identifier.</figcaption></figure>
<figure class="shot"><img src="/screenshots/vulnerability-detail.png" alt="GitLab vulnerability page for GL011: Download-and-execute pattern in script (curl | bash). Remediation: See https://glsec.github.io/rules/GL011/. Report type SAST, scanner glsec, file bad/.gitlab-ci.yml line 5 with the source line curl -fsSL https://example.com/install.sh | sh, the finding's message with the job name, and the identifiers GL011 and CICD-SEC-3. Status Needs triage, severity High" width="1143" height="730" loading="lazy"><figcaption>One vulnerability: the source line, glsec's message, and the link to the rule page as remediation.</figcaption></figure>

<h2 id="merge-request">In the merge request</h2>
<p>The merge request's security widget lists the findings the merge request adds, compared with the target branch.</p>
<figure class="shot"><img src="/screenshots/mr-security-widget.png" alt="GitLab merge request widget: Security scanning detected 3 new potential vulnerabilities, 0 critical, 1 high, and 2 others, with a View report button" width="811" height="75" loading="lazy"><figcaption>The security widget of a merge request that adds a job with three findings.</figcaption></figure>
<figure class="shot"><img src="/screenshots/mr-security-report.png" alt="GitLab merge request Reports tab, Security scan: SAST detected 3 new potential vulnerabilities. New: High GL060 docker run mounting sensitive host paths, Medium GL054 Docker-in-Docker service implies a privileged runner, Medium GL056 docker run --privileged in CI script" width="1143" height="275" loading="lazy"><figcaption>The same merge request's Reports tab, with the three new findings.</figcaption></figure>
<p><strong>One limitation:</strong> GitLab matches a finding from one pipeline to the next by file and line. A merge request that adds or removes lines above an unchanged finding shows it as fixed and as new at the same time. A <code>tracking</code> signature in the report didn't change that in the test, so glsec's report has none.</p>

<h2 id="pipeline-execution-policy">Making glsec mandatory: a pipeline execution policy</h2>
<p>A <a href="https://docs.gitlab.com/user/application_security/policies/pipeline_execution_policies/">pipeline execution policy</a> runs glsec in every pipeline of the projects it applies to, and the projects can neither remove it nor configure it away. Two files in the security policy project:</p>
<p><code>.gitlab/security-policies/policy.yml</code></p>
<pre tabindex="0"><code>pipeline_execution_policy:
- name: glsec
description: Lint .gitlab-ci.yml with glsec in every pipeline.
enabled: true
pipeline_config_strategy: inject_policy
content:
include:
- project: my-group/security-policies # this policy project
file: glsec-policy.yml
ref: main</code></pre>
<p><code>glsec-policy.yml</code></p>
<pre tabindex="0"><code>include:
- component: gitlab.com/glsec-io/glsec/glsec@{{.Component}}
inputs:
version: "{{.Version}}"
stage: .pipeline-policy-pre
no_repo_config: true
no_ignores: true
strict: true # optional: warnings fail the job too

# The policy configuration is evaluated on its own, without the project's
# workflow:rules. A job without rules: then runs only in branch and tag
# pipelines, so without this the merge request pipelines get no scan.
glsec:
rules:
- when: always</code></pre>
<p>What it did in the test:</p>
<ul>
<li>The job runs in the reserved stage <code>.pipeline-policy-pre</code>, before every project job. When it fails, the project's jobs are skipped.</li>
<li>A merge request that adds a <code>.glsec.yml</code> or a <code># glsec:ignore</code> comment changes nothing: <code>no_repo_config</code> and <code>no_ignores</code> make glsec ignore both, and the job log says <code>note: .glsec.yml is ignored (--no-repo-config)</code>.</li>
<li>The component reference needs the <code>v</code> of the tag (<code>@{{.Component}}</code>). A reference GitLab can't resolve fails every pipeline of every project the policy applies to.</li>
<li>If a project has its own job named <code>glsec</code>, GitLab renames the policy job to <code>glsec:policy-&lt;id&gt;-0</code>, and both run.</li>
<li>The policy also runs in branch pipelines that the project's <code>workflow:rules</code> don't create, so a push to a merge request branch gets a branch pipeline with only the policy job, next to the merge request pipeline.</li>
</ul>
<p>To upgrade, bump the component and <code>version</code> in the policy project; the projects don't change. The <a href="https://gitlab.com/glsec-io/glsec#enforcing-glsec-with-a-pipeline-execution-policy">component README</a> keeps the same example.</p>

<h2 id="scan-execution-policy">Scan execution policies and GL082</h2>
<p><a href="/rules/GL082/">GL082</a> reports variables such as <code>SAST_DISABLED</code> that switch off GitLab's security scans. With a <a href="https://docs.gitlab.com/user/application_security/policies/scan_execution_policies/">scan execution policy</a> they don't: in the test, a policy running <code>scan: sast</code> on every branch still created its <code>semgrep-sast-0</code> job after a commit that set <code>SAST_DISABLED: "true"</code>, where the template include created no SAST job at all. So a scan execution policy is the way to make a scan mandatory, and GL082 still matters for projects and scans the policy doesn't cover. <a href="/rules/GL082/#scan-execution-policies">More in GL082</a>.</p>

<p class="source">Tiers as listed in GitLab's documentation for the <a href="https://docs.gitlab.com/user/application_security/vulnerability_report/">vulnerability report</a>, <a href="https://docs.gitlab.com/user/application_security/policies/pipeline_execution_policies/">pipeline execution policies</a> and <a href="https://docs.gitlab.com/user/application_security/policies/scan_execution_policies/">scan execution policies</a>. GitLab is a trademark of GitLab Inc.</p>
</main>
{{template "foot" .}}{{end}}

{{define "performance.html"}}{{template "head" .}}
<main class="wrap doc">
<h1>Performance</h1>
Expand Down
3 changes: 2 additions & 1 deletion testdata/rules.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[
{"id": "GL001", "severity": "error", "owasp": ["CICD-SEC-3"], "description": "Mutable image tag"},
{"id": "GL002", "severity": "warn", "owasp": ["CICD-SEC-4", "CICD-SEC-8"], "description": "Second rule"},
{"id": "GL011", "severity": "error", "owasp": ["CICD-SEC-3"], "description": "Remote script piped to a shell"}
{"id": "GL011", "severity": "error", "owasp": ["CICD-SEC-3"], "description": "Remote script piped to a shell"},
{"id": "GL082", "severity": "warn", "owasp": ["CICD-SEC-3"], "description": "Security scan disabled by a variable"}
]
7 changes: 7 additions & 0 deletions testdata/src/docs/rules/GL082.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# GL082 — Security scan disabled by a variable

Text.

## Scan execution policies

Text.
Loading