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_types evaluation or the licenses evaluation 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.

  1. Enable the feature flag on the rails console:

    Feature.enable(:license_types_unknown_expression_fallback)
  2. Create a project.

  3. Go to Secure > Policies.

  4. Select New policy.

  5. Select Merge request approval policy.

  6. Switch to .yaml mode.

  7. 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
  8. Merge the merge request this creates, to activate the policy.

  9. Add an empty Gemfile.lock file to the project.

  10. Add a file called gl-sbom-gem-bundler.cdx.json reporting the license expression MIT 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"
                        }
                    }
                ]
            }
        ]
    }
  11. Create a new merge request adding a .gitlab-ci.yml file 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
  12. Wait for that merge request's pipeline to finish.

  13. 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 unknown is allowed and the expression fell back to it.

  14. Edit the policy created in step 7, keeping license_types: [unknown] unchanged, and switch match_on_inclusion_license to true (a deny-list policy explicitly denying unknown, with no other policy configured). Merge that change.

  15. 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_reason attribute 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.
Edited by Marcos Rocha

Merge request reports

Loading
Loading