Align license compliance widget with license_types fallback
This MR is stacked on !256826 (merged) and targets its branch, since it needs the license_types_unknown_expression_fallback feature flag definition that MR introduces. GitLab will retarget this to master automatically once that MR merges.
What does this MR do and why?
SCA::LicenseCompliance (the class backing the Reports > License compliance merge request widget) evaluates a compound SPDX expression by checking each operand individually against a project's configured license policies. This is correct for the newer licenses policy property, but it disagreed with !256826 (merged)'s fix to the actual approval decision for the legacy license_types property (Security::ScanResultPolicies::LicenseViolationChecker), which instead collapses any compound expression to the unknown sentinel. That MR intentionally left this widget untouched, so a license_types policy that allowed unknown correctly did not block the merge request, but the widget still showed the dependency as denied.
allowed_license_expression? now falls back the whole expression to unknown too, behind the same license_types_unknown_expression_fallback flag, but only when the applicable policy is license_types-sourced. The newer licenses property keeps evaluating operands individually, unchanged. SoftwareLicensePolicy records are created for both properties and were previously merged into one undifferentiated map; they are now split via Security::ApprovalPolicyRule#license_types.
Plus-suffixed licenses (e.g. LGPL-2.0+) are excluded from the fallback regardless of policy source, matching !256826 (merged)'s own MULTI_LICENSE_NODE_TYPES exclusion, since they're a single license with an "or later" modifier, not a compound expression.
denied_license_policies_from_report previously required an allow-list policy to exist anywhere in the project before evaluating a compound expression at all. That gate correctly protects the older "deny anything unmatched" allow-list default, but it also blocked the new unknown fallback from ever running, so a project whose only license_types policy is deny-list oriented always showed a compound expression as unclassified, even when that policy explicitly denies unknown and the real approval flow would block the merge request. A compound expression is now always evaluated through the fallback when a license_types policy exists, regardless of that policy's direction; the default-deny-unmatched behavior for non-compound licenses stays gated on an allow-list policy existing.
Design decisions
- Why a project with both policy types combines them as "denied wins". A compound expression is denied if either the
license_typesevaluation or thelicensesevaluation would deny it. This matches how multiple simultaneous policies already combined before this change, so mixing both properties in one project doesn't get a new, more lenient outcome.
How to set up and validate locally
Preconditions: GitLab Ultimate. Merge request approval policies are an Ultimate-tier feature.
-
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 that merge request's pipeline to finish.
-
Check that merge request's Reports > License compliance widget. Verify the dependency shows as Allowed, matching the fact that the merge request itself is not blocked, since
unknownis allowed and the expression fell back to it. -
Edit the policy created in step 7, keeping
license_types: [unknown]unchanged, and switchmatch_on_inclusion_licensetotrue(a deny-list policy explicitly denyingunknown, with no other policy configured). Merge that change. -
Check the widget again. Verify it now shows the dependency as Denied, matching the merge request now requiring approval, even though no allow-list policy exists anywhere in the project.
References
- Fixes #630445
- Discovered while validating !256826 (merged)
- Followed up by !257372 (merged) (open, draft), adding a
classification_reasonattribute recording why a license was classified allowed or denied under the license_types unknown fallback. - Followed up by !257376 (merged) (open, draft), stacked on the MR above, surfacing that reason as a message in the License compliance widget.
- Followed up by !257404 (merged) (open, draft), converting the remaining strong_memoize block-form calls in SCA::LicenseCompliance to strong_memoize_attr.