diff --git a/.github/site-checks/pages.txt b/.github/site-checks/pages.txt index 3847703..8514d1a 100644 --- a/.github/site-checks/pages.txt +++ b/.github/site-checks/pages.txt @@ -4,4 +4,5 @@ /rules/GL001/ /owasp/ /performance/ +/gitlab-ultimate/ /404.html diff --git a/README.md b/README.md index 287e9bb..abc4c45 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Source of , 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. diff --git a/main.go b/main.go index 32e83ca..09d074b 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/main_test.go b/main_test.go index 615b37b..ae57edb 100644 --- a/main_test.go +++ b/main_test.go @@ -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) } @@ -88,7 +88,7 @@ func TestBuild_HomeAndOverview(t *testing.T) { "mise use github:glsec/glsec", "pipx install glsec", `GitLab`, - "3 rules", + "4 rules", } { if !strings.Contains(home, want) { t.Errorf("home page lacks %s", want) @@ -184,7 +184,7 @@ func TestBuild_OWASPPage(t *testing.T) { `id="cicd-sec-3"`, `GL001 Mutable image tag`, `GL011`, - "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", @@ -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{ + "

GitLab Ultimate: vulnerability report and policies

", + "component: gitlab.com/glsec-io/glsec/glsec@v1.0.25", + `version: "1.23.0"`, + `
`, + ``, + } { + 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")), `screenshots`) { + 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"} { diff --git a/reports.go b/reports.go index d5d93cb..4e27b63 100644 --- a/reports.go +++ b/reports.go @@ -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 (screenshots). 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}} diff --git a/static/screenshots/mr-security-report.png b/static/screenshots/mr-security-report.png new file mode 100644 index 0000000..cfb1ed1 Binary files /dev/null and b/static/screenshots/mr-security-report.png differ diff --git a/static/screenshots/mr-security-widget.png b/static/screenshots/mr-security-widget.png new file mode 100644 index 0000000..3e533f4 Binary files /dev/null and b/static/screenshots/mr-security-widget.png differ diff --git a/static/screenshots/vulnerability-detail.png b/static/screenshots/vulnerability-detail.png new file mode 100644 index 0000000..daef9c0 Binary files /dev/null and b/static/screenshots/vulnerability-detail.png differ diff --git a/static/screenshots/vulnerability-report.png b/static/screenshots/vulnerability-report.png new file mode 100644 index 0000000..b6a041f Binary files /dev/null and b/static/screenshots/vulnerability-report.png differ diff --git a/templates/pages.html b/templates/pages.html index 014d9fb..9a3eb54 100644 --- a/templates/pages.html +++ b/templates/pages.html @@ -81,6 +81,68 @@

Pipeline inventory (CycloneDX) glsec inventory

{{template "foot" .}}{{end}} +{{define "ultimate.html"}}{{template "head" .}} +
+

GitLab Ultimate: vulnerability report and policies

+

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.

+ +

Findings in the vulnerability report

+

With --format gitlab-sast, glsec writes a SAST report in GitLab's security report schema. A job uploads it as artifacts:reports:sast (the job and an example report), 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.

+
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
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.
+
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
One vulnerability: the source line, glsec's message, and the link to the rule page as remediation.
+ +

In the merge request

+

The merge request's security widget lists the findings the merge request adds, compared with the target branch.

+
GitLab merge request widget: Security scanning detected 3 new potential vulnerabilities, 0 critical, 1 high, and 2 others, with a View report button
The security widget of a merge request that adds a job with three findings.
+
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
The same merge request's Reports tab, with the three new findings.
+

One limitation: 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 tracking signature in the report didn't change that in the test, so glsec's report has none.

+ +

Making glsec mandatory: a pipeline execution policy

+

A pipeline execution policy 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:

+

.gitlab/security-policies/policy.yml

+
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
+

glsec-policy.yml

+
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
+

What it did in the test:

+ +

To upgrade, bump the component and version in the policy project; the projects don't change. The component README keeps the same example.

+ +

Scan execution policies and GL082

+

GL082 reports variables such as SAST_DISABLED that switch off GitLab's security scans. With a scan execution policy they don't: in the test, a policy running scan: sast on every branch still created its semgrep-sast-0 job after a commit that set SAST_DISABLED: "true", 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. More in GL082.

+ +

Tiers as listed in GitLab's documentation for the vulnerability report, pipeline execution policies and scan execution policies. GitLab is a trademark of GitLab Inc.

+
+{{template "foot" .}}{{end}} + {{define "performance.html"}}{{template "head" .}}

Performance

diff --git a/testdata/rules.json b/testdata/rules.json index c8cdcc8..588d32c 100644 --- a/testdata/rules.json +++ b/testdata/rules.json @@ -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"} ] diff --git a/testdata/src/docs/rules/GL082.md b/testdata/src/docs/rules/GL082.md new file mode 100644 index 0000000..179068f --- /dev/null +++ b/testdata/src/docs/rules/GL082.md @@ -0,0 +1,7 @@ +# GL082 — Security scan disabled by a variable + +Text. + +## Scan execution policies + +Text.