anchore / syft

CLI tool and library for generating a Software Bill of Materials from container images and filesystems
Apache License 2.0
6.31k stars 578 forks source link

Duplicate entry in SBOM with local NPM dependencies #2559

Open atl-mk opened 10 months ago

atl-mk commented 10 months ago

What happened:

Syft creates two entries in the SBOM for the local dependency, one of which doesn't have the details like version or license

What you expected to happen:

For there to only be one entry in the SBOM output

Steps to reproduce the issue:

  1. Create a package.json for your main project, e.g.
{
  "name": "test",
  "private": true,
  "dependencies": {
    "jquery": "file:./packages/example"
  }
}
  1. Create a local dependency, e.g. un the relative folder ./packages/example

  2. Create a package.json file for the local dependency, e.g.

{
  "name": "example",
  "private": true,
  "version": "8.8.8",
  "license": "Apache-2.0"
}
  1. Build and run Syft to generate the SBOM like so

npm i && syft . -o cyclonedx-json=sbom.cyclonedx.json --exclude './packages/*'

  1. Read the SBOM, Syft has created two components based on reading the package-lock.json, one for the declared dependency in the package.json, and another from NPM resolving it to the local dependency.

E.g.

{
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "serialNumber": "urn:uuid:7e154fa4-8d8a-41ed-aaba-6f67b5659164",
  "version": 1,
  "metadata": {
    "timestamp": "2024-01-29T21:41:02+01:00",
    "tools": {
      "components": [
        {
          "type": "application",
          "author": "anchore",
          "name": "syft",
          "version": "0.101.1"
        }
      ]
    },
    "component": { "bom-ref": "af63bd4c8601b7f1", "type": "file", "name": "." }
  },
  "components": [
    {
      "bom-ref": "pkg:npm/example?package-id=da2139eb2ab28c91",
      "type": "library",
      "name": "example",
      "cpe": "cpe:2.3:a:example:example:*:*:*:*:*:*:*:*",
      "purl": "pkg:npm/example",
      "properties": [
        {
          "name": "syft:package:foundBy",
          "value": "javascript-lock-cataloger"
        },
        { "name": "syft:package:language", "value": "javascript" },
        { "name": "syft:package:type", "value": "npm" },
        {
          "name": "syft:package:metadataType",
          "value": "javascript-npm-package-lock-entry"
        },
        { "name": "syft:location:0:path", "value": "/package-lock.json" }
      ]
    },
    {
      "bom-ref": "pkg:npm/packages/example@8.8.8?package-id=4043fd3647ed6400",
      "type": "library",
      "name": "packages/example",
      "version": "8.8.8",
      "licenses": [{ "license": { "id": "Apache-2.0" } }],
      "cpe": "cpe:2.3:a:packages\\/example:packages\\/example:8.8.8:*:*:*:*:*:*:*",
      "purl": "pkg:npm/packages/example@8.8.8",
      "properties": [
        {
          "name": "syft:package:foundBy",
          "value": "javascript-lock-cataloger"
        },
        { "name": "syft:package:language", "value": "javascript" },
        { "name": "syft:package:type", "value": "npm" },
        {
          "name": "syft:package:metadataType",
          "value": "javascript-npm-package-lock-entry"
        },
        { "name": "syft:location:0:path", "value": "/package-lock.json" }
      ]
    },
    {
      "bom-ref": "pkg:npm/test?package-id=d4cce09498717bb5",
      "type": "library",
      "name": "test",
      "cpe": "cpe:2.3:a:test:test:*:*:*:*:*:*:*:*",
      "purl": "pkg:npm/test",
      "properties": [
        {
          "name": "syft:package:foundBy",
          "value": "javascript-lock-cataloger"
        },
        { "name": "syft:package:language", "value": "javascript" },
        { "name": "syft:package:type", "value": "npm" },
        {
          "name": "syft:package:metadataType",
          "value": "javascript-npm-package-lock-entry"
        },
        { "name": "syft:location:0:path", "value": "/package-lock.json" }
      ]
    }
  ]
}

Anything else we need to know?:

Purged the package-lock.json and node_modules folder before npm i and generating the SBOMs just to make sure it's fresh

Environment:

Application: syft
Version:    0.101.1
BuildDate:  2024-01-19T22:02:04Z
GitCommit:  3eab5932e5271eea5506ab9710239b1415c827f8
GitDescription: v0.101.1
Platform:   linux/amd64
GoVersion:  go1.21.5
Compiler:   gc

WSL2 Ubuntu 22.04

NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.3 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
spiffcs commented 10 months ago

@atl-mk I definitely see your point that the "link" package in the package-lock.json is does not seem to be handled correctly here.

I'm unsure on which package do you think should be dropped here.

Is exclude working on the way you expect it ?

Do you think that we should be dropping the package that is under the path that was excluded?

Or

Do you think we should be dropping the link packages and not reporting on those as part of the SBOM?

How do you think these "link" packages should be reported as so that the SBOM doesn't lose this information and can express the resolved nature of what was declared in the package.json vs what was cataloged by syft?

cc @willmurphyscode

atl-mk commented 10 months ago

@spiffcs Exclude has no effect here, because Syft is only creating the SBOM from the package-lock.json file. It doesn't matter if the exclude argument is used. I only included it in the reproduction steps to show that's not the issue.

The top and middle one should be merged, like so:

    {
      "bom-ref": "pkg:npm/example?package-id=4790f192e386e4d1",
      "type": "library",
      "name": "example",
      "version": "8.8.8",
      "licenses": [
        {
          "license": {
            "id": "Apache-2.0"
          }
        }
      ],
      "cpe": "cpe:2.3:a:packages\\/example:packages\\/example:8.8.8:*:*:*:*:*:*:*",
      "purl": "pkg:npm/packages/example@8.8.8",
      "properties": [
        {
          "name": "syft:package:foundBy",
          "value": "javascript-lock-cataloger"
        },
        {
          "name": "syft:package:language",
          "value": "javascript"
        },
        {
          "name": "syft:package:type",
          "value": "npm"
        },
        {
          "name": "syft:package:metadataType",
          "value": "javascript-npm-package-lock-entry"
        },
        {
          "name": "syft:location:0:path",
          "value": "/package-lock.json"
        }
      ]
    },

The name should reflect what is declared normally (just example), and CPE+PackageURL should map to the local files on disk.

atl-mk commented 10 months ago

For reference, this is what the package-lock.json file looks like

{
  "name": "test",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "test",
      "dependencies": {
        "example": "file:./packages/example"
      }
    },
    "node_modules/example": {
      "resolved": "packages/example",
      "link": true
    },
    "packages/example": {
      "version": "8.8.8",
      "license": "Apache-2.0"
    }
  }
}

So clearly Syft has some level of resolving linked dependencies from the package-lock.json, I don't see any reason to report the dependency alias.

Here's a related use-case that I'm not personally worried about, but could be of interest to you: https://gist.github.com/nandorojo/1b969a0d88cf81ca8a2a334a5bd2ee4a

spiffcs commented 9 months ago

@anchore/tools I've added this one to Ready.

Based on our team sync we have agreed on removing node_modules/example from the final SBOM.

Here is the documentation for package-lock.json: https://docs.npmjs.com/cli/v9/configuring-npm/package-lock-json#packages

There does not seem to be extra information in the "link" package so taking one over the other (not merging) should be the action here.

No relationships need to be updated for this ticket.

willmurphyscode commented 12 hours ago

This was also reported on discourse as causing false positives in Grype: https://anchorecommunity.discourse.group/t/grype-refers-to-file-in-repo-after-nextjs-upgrade/252/6?u=willmurphy