Fall back to unknown for license_types policies on SPDX expressions
What does this MR do and why?
Security::ScanResultPolicies::LicenseViolationChecker (the class backing license_types policies) compares report license names literally against a policy's configured license_types names. PMDB v3 started returning real SPDX expressions for packages that previously had no classification and came back as the literal unknown sentinel, so those packages now silently escape any license_types policy that allowed or denied unknown, since the raw expression string never equals a configured name.
Behind the license_types_unknown_expression_fallback feature flag, the checker now substitutes the unknown sentinel for a report license name whenever it's a genuine multi-license SPDX expression (AND/OR/WITH), restoring the pre-v3 behavior. A single +-suffixed identifier (e.g. LGPL-2.0+) is deliberately excluded from that check: it's one license with an "or later" modifier, not a multi-license expression, and already carries a real, matchable identifier.
The licenses property already parses expressions correctly via LicenseExpressionChecker and is untouched here, as is the shared Gitlab::LicenseScanning::PackageLicenses producer both paths read from. A small shared Gitlab::SPDX::ExpressionParsing concern now holds the memoized-parse-by-name pattern both this checker and LicenseExpressionChecker need, so the two don't each carry their own copy.
Design decisions
- Why
WITHcollapses tounknownhere, unlike inLicenseExpressionChecker. The SPDX-awarelicensesproperty path resolves aWITHexception by matching its base license first (Gitlab::SPDX::ExpressionEvaluator#evaluate_with). This MR intentionally does not do the same forlicense_types: the goal here is restoring the pre-PMDB-v3 behavior forlicense_types, and v2 had no base-license matching for any expression shape,WITHincluded. TreatingWITHlikeAND/OR(collapse tounknown) is consistent with that goal; treating it like:plus(match the base) would be a new capabilitylicense_typesnever had, not a restoration. - Why a custom license name equal to a raw compound-expression text does not match. An earlier version of this change also allowed policy-side comparison against raw text, but this approach had a bug: when one compound expression was matched and normalized to
unknown, that sentinel entered a shared comparison set, causing an unrelated compound expression elsewhere in the same report to be incorrectly swept into the match. Restricting normalization to the report side only removes this risk structurally, since each report license's match decision now depends only on its own value. The tradeoff is that custom license names matching raw text become inert; referenceunknowndirectly inlicense_typespolicies to match compound expressions. - Why compound-expression dependencies collapse to
unknownand get merged across distinct expressions. Thelicense_typesproperty predates PMDB v3's SPDX-expression support. Before v3, every unclassified package's license was the literal stringunknown, with no way to see or report a more specific value. This MR restores that pre-v3 behavior intentionally:license_typesshould not gain any partial SPDX-expression capability, because the goal is migrating users to the newerlicensesproperty. Treating all compound expressions (AND,OR,WITH) asunknownis consistent with that goal. As a side effect, when multiple dependencies in the same report have different compound expressions and are all denied, they get merged into oneunknownentry with combined dependency names, because each license is now normalized to the same sentinel. @alan confirmed this approach in a comment on the tracking issue: https://gitlab.com/gitlab-com/request-for-help/-/work_items/5401#note_3880678670
Known limitation
- The Reports > License compliance widget can disagree with the approval decision for compound expressions. That widget is rendered by
SCA::LicenseCompliance, a separate class fromLicenseViolationChecker, and evaluates a compound expression by checking each operand individually rather than falling back tounknown. Alicense_typespolicy that allowsunknowncorrectly does not block the merge request, but the widget can still show the compound-expression license as denied. Fixing the widget is out of scope here to keep this MR's change minimal; tracked as a follow-up: #630445
How to set up and validate locally
Preconditions: none beyond the flag below.
-
Enable the feature flag on the rails console:
Feature.enable(:license_types_unknown_expression_fallback) -
Create a project.
-
Go to Secure > Policies.
-
Select New policy.
-
Select Merge request approval policy.
-
Switch to .yaml mode.
-
Paste the following policy:
approval_policy: - name: Approved licenses enabled: true rules: - type: license_finding match_on_inclusion_license: false license_states: - newly_detected branch_type: protected license_types: - unknown actions: - type: require_approval approvals_required: 1 role_approvers: - developer - type: send_bot_message enabled: true -
Merge the merge request this creates, to activate the policy.
-
Add an empty
Gemfile.lockfile to the project. -
Add a file called
gl-sbom-gem-bundler.cdx.jsonreporting the license expressionMIT AND Apache-2.0:{ "bomFormat": "CycloneDX", "specVersion": "1.4", "serialNumber": "urn:uuid:a15e529c-2113-4a11-a694-6bc3ea4e2b53", "version": 1, "metadata": { "timestamp": "2022-02-23T08:02:39Z", "tools": [ { "vendor": "GitLab", "name": "Gemnasium", "version": "2.34.0" } ], "authors": [ { "name": "GitLab", "email": "support@gitlab.com" } ], "properties": [ { "name": "gitlab:dependency_scanning:input_file:path", "value": "Gemfile.lock" }, { "name": "gitlab:dependency_scanning:package_manager:name", "value": "bundler" }, { "name": "gitlab:meta:schema_version", "value": "1" } ] }, "components": [ { "name": "sidekiq", "version": "4.2.10", "purl": "pkg:gem/sidekiq@4.2.10", "type": "library", "bom-ref": "pkg:gem/sidekiq@4.2.10", "licenses": [ { "license": { "name": "MIT AND Apache-2.0" } } ] } ] } -
Create a new merge request adding a
.gitlab-ci.ymlfile with the content:include: - template: Jobs/Dependency-Scanning.gitlab-ci.yml gemnasium-dependency_scanning: stage: test script: 'pwd' artifacts: reports: cyclonedx: gl-sbom-gem-bundler.cdx.json -
Wait for the pipeline to finish.
-
Verify the merge request is not blocked and does not require approval, because
unknownis allowed and the expression fell back to it. -
Edit the policy to remove
unknownfromlicense_types(or deny it, by switchingmatch_on_inclusion_licensetotrue). -
Merge the resulting policy merge request, to apply the change.
-
Verify the merge request is now blocked and requires approval, and that the security bot comment lists the dependency under
unknown, proving the compound expression is treated asunknownrather than expanded or silently dropped. -
Negative path: edit the policy again to restore step 7's configuration (
license_types: [unknown],match_on_inclusion_license: false), and merge that change. -
Disable the feature flag on the rails console:
Feature.disable(:license_types_unknown_expression_fallback) -
Retry the pipeline on the merge request created in step 11 (Pipelines tab > Retry), so the check re-evaluates with the flag disabled. Nothing else triggers a re-check here, since the flag toggle itself changes neither the pipeline nor the policy.
-
Verify the merge request is blocked and requires approval even though
unknownis allowed, showing today's behavior (without the fix) is unchanged with the flag off.
References
- Closes https://gitlab.com/gitlab-com/request-for-help/-/work_items/5401
- Follow-up: #630445