diff --git a/.gitignore b/.gitignore index 50a69abc..008c5a75 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ build target .npmrc src/providers/*.wasm +*.egg-info diff --git a/CONVENTIONS.md b/CONVENTIONS.md new file mode 100644 index 00000000..a58277c2 --- /dev/null +++ b/CONVENTIONS.md @@ -0,0 +1,99 @@ +# Coding Conventions + + + +## Language and Framework + +- **Primary Language**: JavaScript (ES modules, `"type": "module"` in package.json) +- **TypeScript**: Configuration present but code is primarily JavaScript with JSDoc +- **Node.js**: Requires Node >= 20.0.0, npm >= 11.5.1 +- **CLI**: `yargs` for command-line argument parsing +- **Parsing Libraries**: `fast-xml-parser`, `fast-toml`, `smol-toml`, `tree-sitter-requirements` + +## Code Style + +- **Linter**: ESLint with recommended config + editorconfig + import plugins +- **Indentation**: Tabs (4 spaces for YAML/Markdown) +- **Line endings**: LF +- **Max line length**: 100 (120 for Markdown) +- **Charset**: UTF-8, final newline, trim trailing whitespace +- **Import ordering** (ESLint enforced): builtin, external, internal, parent, sibling, index — alphabetical within groups +- **Strict equality**: `eqeqeq: ["warn", "always", {"null": "never"}]` +- **Curly braces**: Required (`curly: "warn"`) +- **No throw literals**: `no-throw-literal: "warn"` +- **No Prettier** — ESLint + EditorConfig handle formatting + +## Naming Conventions + +- **Classes**: PascalCase with underscore-separated language names (`Java_maven`, `Base_java`, `Javascript_npm`) +- **Files**: snake_case for providers (`base_java.js`, `javascript_npm.js`, `python_pip.js`) +- **Test files**: `*.test.js` suffix (`analysis.test.js`, `provider.test.js`) +- **Functions/Methods**: camelCase (`provideComponent()`, `provideStack()`, `validateLockFile()`) +- **Variables**: camelCase (`manifestPath`, `backendUrl`) +- **Constants**: UPPER_SNAKE_CASE (`ecosystem_maven`, `DEFAULT_WORKSPACE_DISCOVERY_IGNORE`) +- **Private class fields**: `#` prefix (`#manifest`, `#cmd`, `#ecosystem`) +- **Protected methods**: `_` prefix (`_lockFileName()`, `_cmdName()`, `_listCmdArgs()`) + +## File Organization + +``` +src/ +├── index.js # Main export +├── cli.js # CLI entry point +├── analysis.js # API request handling +├── provider.js # Provider matching logic +├── workspace.js # Workspace discovery +├── tools.js # Utilities +├── sbom.js # SBOM handling +├── cyclone_dx_sbom.js # CycloneDX SBOM generation +├── providers/ # Ecosystem providers +│ ├── base_java.js +│ ├── base_javascript.js +│ ├── java_maven.js +│ ├── javascript_npm.js +│ ├── python_pip.js +│ ├── rust_cargo.js +│ └── processors/ # Specialized processors +├── license/ # License detection +└── oci_image/ # OCI image analysis + +test/ +├── analysis.test.js +├── provider.test.js +├── tools.test.js +└── providers/ # Provider-specific tests +``` + +## Error Handling + +- **Throw Error objects**: `throw new Error("message")`, `throw new TypeError("message")` +- **No custom error classes** — uses built-in `Error` and `TypeError` +- **HTTP errors**: Check `resp.status`, throw with status code and response text +- **Async errors**: Bubble up naturally via async/await (no blanket try-catch) +- **Validation errors**: Thrown early with descriptive context (manifest type, lock file) + +## Testing Conventions + +- **Framework**: Mocha with TDD UI (`suite()` / `test()`) +- **Assertions**: Chai with `expect()` syntax +- **Mocking**: Sinon for stubs; MSW (Mock Service Worker) for HTTP mocking +- **Module mocking**: `esmock` with experimental loader +- **Coverage**: C8 with 82% line coverage requirement +- **Test patterns**: `expect(res).to.deep.equal(...)`, `expect(() => ...).to.throw('message')` +- **Higher-order setup**: Functions like `interceptAndRun()` for test setup/teardown +- **Prefer real tool invocations over env var overrides**: Tests should call the actual ecosystem tools (pip, uv, poetry, mvn, npm, etc.) rather than injecting pre-recorded output via `TRUSTIFY_DA_*` environment variables. The CI environment has these tools available. Env var overrides (`TRUSTIFY_DA_PIP_REPORT`, `TRUSTIFY_DA_UV_EXPORT`, etc.) exist for users who lack the tool locally, but tests should exercise the real tool path to catch integration issues. +- **Golden SBOM files for every test fixture**: Every provider test fixture directory must include `expected_stack_sbom.json` and `expected_component_sbom.json` golden files. Tests must use the `SBOM_CASES` pattern to do a full `deep.equal` comparison of the provider output against these golden files. Manual partial assertions (e.g. checking a single component name) are not a substitute — they may be added as supplementary tests but never as the only verification for a fixture. + +## Commit Messages + +- Likely Conventional Commits format +- DCO (Developer Certificate of Origin) required +- Semantic versioning (`0.3.0` in package.json) + +## Dependencies + +- **Package manager**: npm with `package-lock.json` +- **Module system**: ES modules with explicit `.js` extensions in relative imports +- **Import convention**: `import fs from 'node:fs'` (node: protocol for built-ins) +- **Environment variables**: Prefixed with `TRUSTIFY_DA_` (e.g., `TRUSTIFY_DA_MVN_PATH`, `TRUSTIFY_DA_TOKEN`, `TRUSTIFY_DA_DEBUG`) +- **Multi-ecosystem support**: npm, pnpm, yarn, Maven, Gradle, pip, cargo, Go modules, Docker/Podman diff --git a/src/provider.js b/src/provider.js index 9a7f3074..5c5d06ec 100644 --- a/src/provider.js +++ b/src/provider.js @@ -8,6 +8,7 @@ import Javascript_npm from './providers/javascript_npm.js'; import Javascript_pnpm from './providers/javascript_pnpm.js'; import Javascript_yarn from './providers/javascript_yarn.js'; import pythonPipProvider from './providers/python_pip.js' +import Python_pip_pyproject from './providers/python_pip_pyproject.js' import Python_poetry from './providers/python_poetry.js' import Python_uv from './providers/python_uv.js' import rustCargoProvider from './providers/rust_cargo.js' @@ -30,6 +31,7 @@ export const availableProviders = [ pythonPipProvider, new Python_poetry(), new Python_uv(), + new Python_pip_pyproject(), rustCargoProvider] /** diff --git a/src/providers/base_pyproject.js b/src/providers/base_pyproject.js index fcc309f7..d745e7bc 100644 --- a/src/providers/base_pyproject.js +++ b/src/providers/base_pyproject.js @@ -250,64 +250,27 @@ export default class Base_pyproject { } /** - * Build dependency tree from graph, starting from direct deps. + * Compute the set of graph nodes reachable from direct deps, excluding ignored. * @param {Map} graph - * @param {string[]} directDeps - canonical names of direct deps + * @param {string[]} directDeps * @param {Set} ignoredDeps - * @param {boolean} includeTransitive - * @returns {DepTreeEntry[]} + * @returns {Set} * @protected */ - _buildDependencyTree(graph, directDeps, ignoredDeps, includeTransitive) { - let result = [] - - for (let key of directDeps) { - if (ignoredDeps.has(key)) { continue } - - let entry = graph.get(key) - if (!entry) { continue } - - let depTree = [] - if (includeTransitive) { - let visited = new Set() - visited.add(key) - this._collectTransitive(graph, entry.children, depTree, ignoredDeps, visited) + _reachableNodes(graph, directDeps, ignoredDeps) { + let reachable = new Set() + let queue = directDeps.filter(k => !ignoredDeps.has(k) && graph.has(k)) + while (queue.length > 0) { + let key = queue.shift() + if (reachable.has(key)) { continue } + reachable.add(key) + for (let child of graph.get(key).children) { + if (!ignoredDeps.has(child) && graph.has(child) && !reachable.has(child)) { + queue.push(child) + } } - - result.push({ name: entry.name, version: entry.version, dependencies: depTree }) - } - - result.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())) - return result - } - - /** - * Recursively collect transitive dependencies. - * @param {Map} graph - * @param {string[]} childKeys - * @param {DepTreeEntry[]} result - mutated in place - * @param {Set} ignoredDeps - * @param {Set} visited - * @returns {void} - * @protected - */ - _collectTransitive(graph, childKeys, result, ignoredDeps, visited) { - for (let childKey of childKeys) { - let canonKey = this._canonicalize(childKey) - if (ignoredDeps.has(canonKey)) { continue } - if (visited.has(canonKey)) { continue } - visited.add(canonKey) - - let entry = graph.get(canonKey) - if (!entry) { continue } - - let childDeps = [] - this._collectTransitive(graph, entry.children, childDeps, ignoredDeps, visited) - - result.push({ name: entry.name, version: entry.version, dependencies: childDeps }) } - - result.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())) + return reachable } /** @@ -320,22 +283,6 @@ export default class Base_pyproject { return new PackageURL('pypi', undefined, name, version, undefined, undefined) } - /** - * Recursively add a dependency and its transitive deps to the SBOM. - * @param {PackageURL} source - * @param {DepTreeEntry} dep - * @param {Sbom} sbom - * @returns {void} - * @private - */ - _addAllDependencies(source, dep, sbom) { - let targetPurl = this._toPurl(dep.name, dep.version) - sbom.addDependency(source, targetPurl) - if (dep.dependencies && dep.dependencies.length > 0) { - dep.dependencies.forEach(child => this._addAllDependencies(this._toPurl(dep.name, dep.version), child, sbom)) - } - } - /** * Create SBOM json string for a pyproject.toml project. * @param {string} manifest - path to pyproject.toml @@ -353,7 +300,6 @@ export default class Base_pyproject { let { directDeps, graph } = await this._getDependencyData(manifestDir, workspaceDir, parsed, opts) let ignoredDeps = this._getIgnoredDeps(manifest) - let dependencies = this._buildDependencyTree(graph, directDeps, ignoredDeps, includeTransitive) let sbom = new Sbom() let rootName = this._getProjectName(parsed) || DEFAULT_ROOT_NAME @@ -362,13 +308,30 @@ export default class Base_pyproject { let license = this.readLicenseFromManifest(manifest) sbom.addRoot(rootPurl, license) - dependencies.forEach(dep => { - if (includeTransitive) { - this._addAllDependencies(rootPurl, dep, sbom) - } else { - sbom.addDependency(rootPurl, this._toPurl(dep.name, dep.version)) + if (includeTransitive) { + let reachable = this._reachableNodes(graph, directDeps, ignoredDeps) + for (let key of directDeps) { + if (!reachable.has(key)) { continue } + let entry = graph.get(key) + sbom.addDependency(rootPurl, this._toPurl(entry.name, entry.version)) } - }) + for (let [key, entry] of graph) { + if (!reachable.has(key)) { continue } + let parentPurl = this._toPurl(entry.name, entry.version) + for (let child of entry.children) { + if (!reachable.has(child)) { continue } + let childEntry = graph.get(child) + sbom.addDependency(parentPurl, this._toPurl(childEntry.name, childEntry.version)) + } + } + } else { + for (let key of directDeps) { + if (ignoredDeps.has(key)) { continue } + let entry = graph.get(key) + if (!entry) { continue } + sbom.addDependency(rootPurl, this._toPurl(entry.name, entry.version)) + } + } return sbom.getAsJsonString(opts) } diff --git a/src/providers/python_pip_pyproject.js b/src/providers/python_pip_pyproject.js new file mode 100644 index 00000000..72354d2b --- /dev/null +++ b/src/providers/python_pip_pyproject.js @@ -0,0 +1,155 @@ +import fs from 'node:fs' +import path from 'node:path' + +import { environmentVariableIsPopulated, getCustomPath, invokeCommand } from '../tools.js' + +import Base_pyproject from './base_pyproject.js' + +/** + * Python provider for pyproject.toml files using PEP 621 format without a lock file. + * Uses `pip install --dry-run --ignore-installed --report` to resolve the full dependency tree. + * Acts as the fallback provider when no lock file (uv.lock/poetry.lock) is found. + */ +export default class Python_pip_pyproject extends Base_pyproject { + + /** @returns {string} */ + _lockFileName() { + return '.pip-lock-nonexistent' + } + + /** @returns {string} */ + _cmdName() { + return 'pip' + } + + /** + * Always returns true — pip provider is the fallback when no lock file is found. + * @param {string} manifestDir + * @param {{}} [opts={}] + * @returns {boolean} + */ + // eslint-disable-next-line no-unused-vars + validateLockFile(manifestDir, opts = {}) { + return true + } + + /** + * Get pip report output from env var override or by running pip. + * @param {string} manifestDir - directory containing pyproject.toml + * @param {{}} [opts={}] + * @returns {string} pip report JSON string + */ + _getPipReportOutput(manifestDir, opts) { + if (environmentVariableIsPopulated('TRUSTIFY_DA_PIP_REPORT')) { + return Buffer.from(process.env['TRUSTIFY_DA_PIP_REPORT'], 'base64').toString('ascii') + } + let pipBin = getCustomPath('pip3', opts) + try { + invokeCommand(pipBin, ['--version']) + } catch { + pipBin = getCustomPath('pip', opts) + } + let eggInfoDirs = this._findEggInfoDirs(manifestDir) + let result = invokeCommand(pipBin, [ + 'install', '--dry-run', '--ignore-installed', '--quiet', '--report', '-', '.' + ], { cwd: manifestDir }).toString() + this._cleanupEggInfo(manifestDir, eggInfoDirs) + return result + } + + /** + * Parse pip report JSON and build dependency graph. + * @param {string} reportJson - pip report JSON string + * @returns {{directDeps: string[], graph: Map}} + */ + _parsePipReport(reportJson) { + let report = JSON.parse(reportJson) + let packages = report.install || [] + + let rootEntry = packages.find(p => p.download_info?.dir_info !== undefined) + let rootRequires = rootEntry?.metadata?.requires_dist || [] + + let directDepNames = new Set() + for (let req of rootRequires) { + if (this._hasExtraMarker(req)) { continue } + let name = this._extractDepName(req) + if (name) { directDepNames.add(this._canonicalize(name)) } + } + + let graph = new Map() + let nonRootPackages = packages.filter(p => p !== rootEntry) + + for (let pkg of nonRootPackages) { + let name = pkg.metadata.name + let version = pkg.metadata.version + let key = this._canonicalize(name) + graph.set(key, { name, version, children: [] }) + } + + for (let pkg of nonRootPackages) { + let key = this._canonicalize(pkg.metadata.name) + let entry = graph.get(key) + let requires = pkg.metadata.requires_dist || [] + for (let req of requires) { + let depName = this._extractDepName(req) + if (!depName) { continue } + let depKey = this._canonicalize(depName) + if (graph.has(depKey)) { + entry.children.push(depKey) + } + } + } + + let directDeps = [...directDepNames].filter(key => graph.has(key)) + return { directDeps, graph } + } + + /** + * Check if a requires_dist entry is an extras-only dependency. + * @param {string} req - e.g. "PySocks!=1.5.7,>=1.5.6; extra == \"socks\"" + * @returns {boolean} + */ + _hasExtraMarker(req) { + return /;\s*.*extra\s*==/.test(req) + } + + /** + * Extract package name from a requires_dist entry. + * @param {string} req - e.g. "charset_normalizer<4,>=2" + * @returns {string|null} + */ + _extractDepName(req) { + let match = req.match(/^([A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)/) + return match ? match[1] : null + } + + /** + * Resolve dependencies using pip install --dry-run --report. + * @param {string} manifestDir + * @param {string} _workspaceDir - unused (pip resolves from manifest directory) + * @param {object} parsed - parsed pyproject.toml + * @param {{}} [opts={}] + * @returns {Promise<{directDeps: string[], graph: Map}>} + */ + // eslint-disable-next-line no-unused-vars + async _getDependencyData(manifestDir, _workspaceDir, parsed, opts) { + let reportOutput = this._getPipReportOutput(manifestDir, opts) + return this._parsePipReport(reportOutput) + } + + _findEggInfoDirs(dir) { + try { + return fs.readdirSync(dir).filter(f => f.endsWith('.egg-info')) + } catch { + return [] + } + } + + _cleanupEggInfo(dir, existing) { + for (let entry of this._findEggInfoDirs(dir)) { + if (!existing.includes(entry)) { + fs.rmSync(path.join(dir, entry), { recursive: true, force: true }) + } + } + } +} diff --git a/test/providers/python_pyproject.test.js b/test/providers/python_pyproject.test.js index 57d64dbc..7be6476e 100644 --- a/test/providers/python_pyproject.test.js +++ b/test/providers/python_pyproject.test.js @@ -4,17 +4,27 @@ import path from 'path' import { expect } from 'chai' import { useFakeTimers } from 'sinon' +import Python_pip_pyproject from '../../src/providers/python_pip_pyproject.js' import Python_poetry from '../../src/providers/python_poetry.js' import Python_uv from '../../src/providers/python_uv.js' let clock -const TIMEOUT = process.env.GITHUB_ACTIONS ? 30000 : 10000 +const TIMEOUT = process.env.GITHUB_ACTIONS ? 30000 : 15000 const uvProvider = new Python_uv() const poetryProvider = new Python_poetry() +const pipProvider = new Python_pip_pyproject() + +const MANIFESTS = 'test/providers/tst_manifests/pyproject' + +const SBOM_CASES = [ + {type: 'stack', method: 'provideStack', fixture: 'expected_stack_sbom.json'}, + {type: 'component', method: 'provideComponent', fixture: 'expected_component_sbom.json'}, +] suite('testing the python-pyproject data provider', () => { + /** Verifies isSupported correctly identifies pyproject.toml manifests. */ [ {name: 'pyproject.toml', expected: true}, {name: 'requirements.txt', expected: false}, @@ -23,49 +33,40 @@ suite('testing the python-pyproject data provider', () => { test(`verify isSupported returns ${testCase.expected} for ${testCase.name}`, () => expect(uvProvider.isSupported(testCase.name)).to.equal(testCase.expected) ) - }) - - test('verify uv validateLockFile returns true when uv.lock exists', () => { - expect(uvProvider.validateLockFile('test/providers/tst_manifests/pyproject/uv_lock')).to.equal(true) - }) - - test('verify uv validateLockFile returns false when uv.lock is missing', () => { - expect(uvProvider.validateLockFile('test/providers/tst_manifests/pyproject/poetry_lock')).to.equal(false) - }) - - test('verify poetry validateLockFile returns true when poetry.lock exists', () => { - expect(poetryProvider.validateLockFile('test/providers/tst_manifests/pyproject/poetry_lock')).to.equal(true) - }) + }); - test('verify poetry validateLockFile returns false when poetry.lock is missing', () => { - expect(poetryProvider.validateLockFile('test/providers/tst_manifests/pyproject/uv_lock')).to.equal(false) + /** Verifies each provider's validateLockFile detects or rejects its lock file. */ + [ + {provider: uvProvider, name: 'uv', dir: 'uv_lock', expected: true}, + {provider: uvProvider, name: 'uv', dir: 'poetry_lock', expected: false}, + {provider: poetryProvider, name: 'poetry', dir: 'poetry_lock', expected: true}, + {provider: poetryProvider, name: 'poetry', dir: 'uv_lock', expected: false}, + ].forEach(({provider, name, dir, expected}) => { + test(`verify ${name} validateLockFile returns ${expected} for ${dir}`, () => { + expect(provider.validateLockFile(`${MANIFESTS}/${dir}`)).to.equal(expected) + }) }) suite('uv projects (via uv export)', () => { - test('verify pyproject.toml sbom provided for stack analysis with uv', async () => { - let expectedSbom = fs.readFileSync('test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_stack_sbom.json').toString() - expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) - let result = await uvProvider.provideStack('test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml') - expect(result).to.deep.equal({ - ecosystem: 'pip', - contentType: 'application/vnd.cyclonedx+json', - content: expectedSbom - }) - }).timeout(TIMEOUT) - - test('verify pyproject.toml sbom provided for component analysis with uv', async () => { - let expectedSbom = fs.readFileSync('test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_component_sbom.json').toString().trim() - expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) - let result = await uvProvider.provideComponent('test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml') - expect(result).to.deep.equal({ - ecosystem: 'pip', - contentType: 'application/vnd.cyclonedx+json', - content: expectedSbom - }) - }).timeout(TIMEOUT) + const fixtureDir = `${MANIFESTS}/pep621_ignore_and_extras` + + /** Verifies stack and component SBOM output matches expected fixtures. */ + SBOM_CASES.forEach(({type, method, fixture}) => { + test(`verify pyproject.toml sbom provided for ${type} analysis with uv`, async () => { + let expectedSbom = fs.readFileSync(path.join(fixtureDir, fixture)).toString().trim() + expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) + let result = await uvProvider[method](path.join(fixtureDir, 'pyproject.toml')) + expect(result).to.deep.equal({ + ecosystem: 'pip', + contentType: 'application/vnd.cyclonedx+json', + content: expectedSbom + }) + }).timeout(TIMEOUT) + }) + /** Verifies exhortignore and trustify-da-ignore markers exclude deps from component analysis. */ test('exhortignore and trustify-da-ignore exclude deps from component analysis', async () => { - let result = await uvProvider.provideComponent('test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml') + let result = await uvProvider.provideComponent(path.join(fixtureDir, 'pyproject.toml')) let sbom = JSON.parse(result.content) let names = sbom.components.map(c => c.name) expect(names).to.not.include('uvicorn') @@ -74,8 +75,9 @@ suite('testing the python-pyproject data provider', () => { expect(names).to.include('requests') }).timeout(TIMEOUT) + /** Verifies ignored transitive deps are pruned from the stack dependency tree. */ test('ignored transitive dep excluded from stack analysis tree', async () => { - let result = await uvProvider.provideStack('test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml') + let result = await uvProvider.provideStack(path.join(fixtureDir, 'pyproject.toml')) let sbom = JSON.parse(result.content) let names = sbom.components.map(c => c.name) expect(names).to.not.include('uvicorn') @@ -85,8 +87,9 @@ suite('testing the python-pyproject data provider', () => { expect(jinja2Dep.dependsOn).to.deep.equal([]) }).timeout(TIMEOUT) + /** Verifies name canonicalization normalizes underscores to hyphens. */ test('name canonicalization: typing_extensions matches typing-extensions', async () => { - let result = await uvProvider.provideComponent('test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml') + let result = await uvProvider.provideComponent(path.join(fixtureDir, 'pyproject.toml')) let sbom = JSON.parse(result.content) let typingExt = sbom.components.find(c => c.name === 'typing-extensions') expect(typingExt).to.exist @@ -95,54 +98,43 @@ suite('testing the python-pyproject data provider', () => { }) suite('uv projects - uv_lock manifest', () => { - test('verify pyproject.toml sbom provided for stack analysis with uv_lock', async () => { - let expectedSbom = fs.readFileSync('test/providers/tst_manifests/pyproject/uv_lock/expected_stack_sbom.json').toString() - expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) - let result = await uvProvider.provideStack('test/providers/tst_manifests/pyproject/uv_lock/pyproject.toml') - expect(result).to.deep.equal({ - ecosystem: 'pip', - contentType: 'application/vnd.cyclonedx+json', - content: expectedSbom - }) - }).timeout(TIMEOUT) - - test('verify pyproject.toml sbom provided for component analysis with uv_lock', async () => { - let expectedSbom = fs.readFileSync('test/providers/tst_manifests/pyproject/uv_lock/expected_component_sbom.json').toString().trim() - expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) - let result = await uvProvider.provideComponent('test/providers/tst_manifests/pyproject/uv_lock/pyproject.toml') - expect(result).to.deep.equal({ - ecosystem: 'pip', - contentType: 'application/vnd.cyclonedx+json', - content: expectedSbom - }) - }).timeout(TIMEOUT) + const fixtureDir = `${MANIFESTS}/uv_lock` + + /** Verifies stack and component SBOM output matches expected fixtures. */ + SBOM_CASES.forEach(({type, method, fixture}) => { + test(`verify pyproject.toml sbom provided for ${type} analysis with uv_lock`, async () => { + let expectedSbom = fs.readFileSync(path.join(fixtureDir, fixture)).toString().trim() + expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) + let result = await uvProvider[method](path.join(fixtureDir, 'pyproject.toml')) + expect(result).to.deep.equal({ + ecosystem: 'pip', + contentType: 'application/vnd.cyclonedx+json', + content: expectedSbom + }) + }).timeout(TIMEOUT) + }) }) suite('poetry projects (via poetry show)', () => { - test('verify pyproject.toml sbom provided for stack analysis with poetry', async () => { - let expectedSbom = fs.readFileSync('test/providers/tst_manifests/pyproject/poetry_lock/expected_stack_sbom.json').toString() - expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) - let result = await poetryProvider.provideStack('test/providers/tst_manifests/pyproject/poetry_lock/pyproject.toml') - expect(result).to.deep.equal({ - ecosystem: 'pip', - contentType: 'application/vnd.cyclonedx+json', - content: expectedSbom - }) - }).timeout(TIMEOUT) - - test('verify pyproject.toml sbom provided for component analysis with poetry', async () => { - let expectedSbom = fs.readFileSync('test/providers/tst_manifests/pyproject/poetry_lock/expected_component_sbom.json').toString().trim() - expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) - let result = await poetryProvider.provideComponent('test/providers/tst_manifests/pyproject/poetry_lock/pyproject.toml') - expect(result).to.deep.equal({ - ecosystem: 'pip', - contentType: 'application/vnd.cyclonedx+json', - content: expectedSbom - }) - }).timeout(TIMEOUT) + const fixtureDir = `${MANIFESTS}/poetry_lock` + + /** Verifies stack and component SBOM output matches expected fixtures. */ + SBOM_CASES.forEach(({type, method, fixture}) => { + test(`verify pyproject.toml sbom provided for ${type} analysis with poetry`, async () => { + let expectedSbom = fs.readFileSync(path.join(fixtureDir, fixture)).toString().trim() + expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) + let result = await poetryProvider[method](path.join(fixtureDir, 'pyproject.toml')) + expect(result).to.deep.equal({ + ecosystem: 'pip', + contentType: 'application/vnd.cyclonedx+json', + content: expectedSbom + }) + }).timeout(TIMEOUT) + }) + /** Verifies resolved versions come from poetry show --all, not dependency constraints. */ test('resolved versions come from poetry show --all, not constraints', async () => { - let result = await poetryProvider.provideStack('test/providers/tst_manifests/pyproject/poetry_lock/pyproject.toml') + let result = await poetryProvider.provideStack(path.join(fixtureDir, 'pyproject.toml')) let sbom = JSON.parse(result.content) let markupsafe = sbom.components.find(c => c.name === 'markupsafe') expect(markupsafe.version).to.equal('3.0.3') @@ -150,8 +142,9 @@ suite('testing the python-pyproject data provider', () => { expect(urllib3.version).to.equal('2.6.3') }).timeout(TIMEOUT) + /** Verifies exhortignore filtering excludes click and its exclusive transitive deps. */ test('exhortignore filtering excludes click and its exclusive transitive deps', async () => { - let result = await poetryProvider.provideStack('test/providers/tst_manifests/pyproject/poetry_lock/pyproject.toml') + let result = await poetryProvider.provideStack(path.join(fixtureDir, 'pyproject.toml')) let sbom = JSON.parse(result.content) let names = sbom.components.map(c => c.name) expect(names).to.not.include('click') @@ -161,31 +154,89 @@ suite('testing the python-pyproject data provider', () => { }) suite('poetry projects - poetry_only_deps manifest', () => { - test('verify pyproject.toml sbom provided for stack analysis with poetry_only_deps', async () => { - let expectedSbom = fs.readFileSync('test/providers/tst_manifests/pyproject/poetry_only_deps/expected_stack_sbom.json').toString() - expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) - let result = await poetryProvider.provideStack('test/providers/tst_manifests/pyproject/poetry_only_deps/pyproject.toml') - expect(result).to.deep.equal({ - ecosystem: 'pip', - contentType: 'application/vnd.cyclonedx+json', - content: expectedSbom - }) + const fixtureDir = `${MANIFESTS}/poetry_only_deps` + + /** Verifies stack and component SBOM output matches expected fixtures. */ + SBOM_CASES.forEach(({type, method, fixture}) => { + test(`verify pyproject.toml sbom provided for ${type} analysis with poetry_only_deps`, async () => { + let expectedSbom = fs.readFileSync(path.join(fixtureDir, fixture)).toString().trim() + expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) + let result = await poetryProvider[method](path.join(fixtureDir, 'pyproject.toml')) + expect(result).to.deep.equal({ + ecosystem: 'pip', + contentType: 'application/vnd.cyclonedx+json', + content: expectedSbom + }) + }).timeout(TIMEOUT) + }) + }) + + /** Verifies the pip provider's validateLockFile always returns true (fallback). */ + test('verify pip validateLockFile always returns true (fallback provider)', () => { + expect(pipProvider.validateLockFile(`${MANIFESTS}/pip_pep621`)).to.equal(true) + expect(pipProvider.validateLockFile('/nonexistent/dir')).to.equal(true) + }) + + suite('pip projects (via pip --dry-run --report)', () => { + const pipFixtureDir = `${MANIFESTS}/pip_pep621` + const pipIgnoreDir = `${MANIFESTS}/pip_pep621_ignore` + + /** Verifies stack and component SBOM output matches expected pip fixtures. */ + SBOM_CASES.forEach(({type, method, fixture}) => { + test(`verify pyproject.toml sbom provided for ${type} analysis with pip`, async () => { + let expectedSbom = fs.readFileSync(path.join(pipFixtureDir, fixture)).toString().trim() + expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) + let result = await pipProvider[method](path.join(pipFixtureDir, 'pyproject.toml')) + expect(result).to.deep.equal({ + ecosystem: 'pip', + contentType: 'application/vnd.cyclonedx+json', + content: expectedSbom + }) + }).timeout(TIMEOUT) + }) + + /** Verifies direct and transitive deps are correctly classified in stack SBOM. */ + test('stack analysis classifies direct and transitive dependencies correctly', async () => { + let result = await pipProvider.provideStack(path.join(pipFixtureDir, 'pyproject.toml')) + let sbom = JSON.parse(result.content) + let rootDep = sbom.dependencies.find(d => d.ref.includes('/test-project@')) + expect(rootDep.dependsOn).to.have.lengthOf(1) + expect(rootDep.dependsOn[0]).to.include('/requests@') + let requestsDep = sbom.dependencies.find(d => d.ref.includes('/requests@')) + let transNames = requestsDep.dependsOn.map(d => d.split('/').pop().split('@')[0]) + expect(transNames).to.include('certifi') + expect(transNames).to.include('charset-normalizer') + expect(transNames).to.include('idna') + expect(transNames).to.include('urllib3') }).timeout(TIMEOUT) - test('verify pyproject.toml sbom provided for component analysis with poetry_only_deps', async () => { - let expectedSbom = fs.readFileSync('test/providers/tst_manifests/pyproject/poetry_only_deps/expected_component_sbom.json').toString().trim() - expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) - let result = await poetryProvider.provideComponent('test/providers/tst_manifests/pyproject/poetry_only_deps/pyproject.toml') - expect(result).to.deep.equal({ - ecosystem: 'pip', - contentType: 'application/vnd.cyclonedx+json', - content: expectedSbom - }) + /** Verifies exhortignore marker produces expected SBOM for stack and component analysis. */ + SBOM_CASES.forEach(({type, method, fixture}) => { + test(`verify exhortignore produces expected sbom for ${type} analysis with pip`, async () => { + let expectedSbom = fs.readFileSync(path.join(pipIgnoreDir, fixture)).toString().trim() + expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) + let result = await pipProvider[method](path.join(pipIgnoreDir, 'pyproject.toml')) + expect(result).to.deep.equal({ + ecosystem: 'pip', + contentType: 'application/vnd.cyclonedx+json', + content: expectedSbom + }) + }).timeout(TIMEOUT) + }) + + /** Verifies name canonicalization (charset_normalizer -> charset-normalizer). */ + test('name canonicalization: charset_normalizer resolved as charset-normalizer', async () => { + let result = await pipProvider.provideStack(path.join(pipFixtureDir, 'pyproject.toml')) + let sbom = JSON.parse(result.content) + let pkg = sbom.components.find(c => c.name === 'charset-normalizer') + expect(pkg).to.exist + expect(pkg.version).to.equal('3.4.7') }).timeout(TIMEOUT) }) + /** Verifies uv and poetry validateLockFile returns false when no lock file is present. */ test('validateLockFile returns false when no lock file is present', () => { - let tmpDir = 'test/providers/tst_manifests/pyproject/no_lock_file_dummy' + let tmpDir = `${MANIFESTS}/no_lock_file_dummy` fs.mkdirSync(tmpDir, { recursive: true }) fs.writeFileSync(`${tmpDir}/pyproject.toml`, '[project]\nname = "test"\nversion = "1.0.0"\ndependencies = ["requests>=2.0"]') @@ -198,18 +249,20 @@ suite('testing the python-pyproject data provider', () => { }) suite('workspace/monorepo support', () => { - const uvWorkspace = 'test/providers/tst_manifests/pyproject/uv_workspace' + const uvWorkspace = `${MANIFESTS}/uv_workspace` + /** Verifies uv walks up to parent directory to find uv.lock. */ test('uv validateLockFile finds uv.lock in parent directory', () => { expect(uvProvider.validateLockFile( path.join(uvWorkspace, 'packages/sub-pkg') )).to.equal(true) }) + /** Verifies poetry does not walk up directories since it has no native workspace support. */ test('poetry validateLockFile does not walk up to parent directory', () => { // Poetry has no native workspace support (python-poetry/poetry#2270). // Each poetry project is treated independently — no lock file walk-up. - let tmpDir = 'test/providers/tst_manifests/pyproject/boundary_test_poetry' + let tmpDir = `${MANIFESTS}/boundary_test_poetry` let subDir = path.join(tmpDir, 'packages', 'child') fs.mkdirSync(subDir, { recursive: true }) fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), @@ -218,18 +271,17 @@ suite('testing the python-pyproject data provider', () => { fs.writeFileSync(path.join(subDir, 'pyproject.toml'), '[tool.poetry]\nname = "child"\nversion = "0.1.0"\n') try { - // poetry.lock exists at root but poetry should NOT walk up expect(poetryProvider.validateLockFile(subDir)).to.equal(false) } finally { fs.rmSync(tmpDir, { recursive: true, force: true }) } }) + /** Verifies lock file search stops at uv workspace root when uv.lock is absent. */ test('validateLockFile stops at uv workspace root boundary when lock file is absent', () => { - let tmpDir = 'test/providers/tst_manifests/pyproject/boundary_test' + let tmpDir = `${MANIFESTS}/boundary_test` let subDir = path.join(tmpDir, 'packages', 'child') fs.mkdirSync(subDir, { recursive: true }) - // root has workspace marker but no lock file fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[tool.uv.workspace]\nmembers = ["packages/*"]\n') fs.writeFileSync(path.join(subDir, 'pyproject.toml'), @@ -241,84 +293,41 @@ suite('testing the python-pyproject data provider', () => { } }) + /** Verifies TRUSTIFY_DA_WORKSPACE_DIR override redirects lock file search. */ test('TRUSTIFY_DA_WORKSPACE_DIR override directs lock file search', () => { let overrideDir = path.resolve(uvWorkspace) expect(uvProvider.validateLockFile( - 'test/providers/tst_manifests/pyproject/poetry_lock', + `${MANIFESTS}/poetry_lock`, { TRUSTIFY_DA_WORKSPACE_DIR: overrideDir } )).to.equal(true) expect(uvProvider.validateLockFile( - 'test/providers/tst_manifests/pyproject/poetry_lock', + `${MANIFESTS}/poetry_lock`, { TRUSTIFY_DA_WORKSPACE_DIR: '/nonexistent/dir' } )).to.equal(false) - }) - - test('verify uv workspace root stack analysis', async () => { - let expectedSbom = fs.readFileSync(path.join(uvWorkspace, 'expected_stack_sbom.json')).toString() - expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) - let result = await uvProvider.provideStack(path.join(uvWorkspace, 'pyproject.toml')) - expect(result).to.deep.equal({ - ecosystem: 'pip', - contentType: 'application/vnd.cyclonedx+json', - content: expectedSbom - }) - }).timeout(TIMEOUT) - - test('verify uv workspace root component analysis', async () => { - let expectedSbom = fs.readFileSync(path.join(uvWorkspace, 'expected_component_sbom.json')).toString().trim() - expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) - let result = await uvProvider.provideComponent(path.join(uvWorkspace, 'pyproject.toml')) - expect(result).to.deep.equal({ - ecosystem: 'pip', - contentType: 'application/vnd.cyclonedx+json', - content: expectedSbom - }) - }).timeout(TIMEOUT) - - test('verify uv workspace mid-package stack analysis', async () => { - let expectedSbom = fs.readFileSync(path.join(uvWorkspace, 'packages/mid-pkg/expected_stack_sbom.json')).toString() - expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) - let result = await uvProvider.provideStack(path.join(uvWorkspace, 'packages/mid-pkg/pyproject.toml')) - expect(result).to.deep.equal({ - ecosystem: 'pip', - contentType: 'application/vnd.cyclonedx+json', - content: expectedSbom + }); + + /** Verifies SBOM output for each workspace package (root, mid-pkg, sub-pkg). */ + [ + {manifestPath: '', label: 'root'}, + {manifestPath: 'packages/mid-pkg', label: 'mid-package'}, + {manifestPath: 'packages/sub-pkg', label: 'sub-package'}, + ].forEach(({manifestPath, label}) => { + const pkgDir = manifestPath ? path.join(uvWorkspace, manifestPath) : uvWorkspace + + SBOM_CASES.forEach(({type, method, fixture}) => { + test(`verify uv workspace ${label} ${type} analysis`, async () => { + let expectedSbom = fs.readFileSync(path.join(pkgDir, fixture)).toString().trim() + expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) + let result = await uvProvider[method](path.join(pkgDir, 'pyproject.toml')) + expect(result).to.deep.equal({ + ecosystem: 'pip', + contentType: 'application/vnd.cyclonedx+json', + content: expectedSbom + }) + }).timeout(TIMEOUT) }) - }).timeout(TIMEOUT) - - test('verify uv workspace mid-package component analysis', async () => { - let expectedSbom = fs.readFileSync(path.join(uvWorkspace, 'packages/mid-pkg/expected_component_sbom.json')).toString().trim() - expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) - let result = await uvProvider.provideComponent(path.join(uvWorkspace, 'packages/mid-pkg/pyproject.toml')) - expect(result).to.deep.equal({ - ecosystem: 'pip', - contentType: 'application/vnd.cyclonedx+json', - content: expectedSbom - }) - }).timeout(TIMEOUT) - - test('verify uv workspace sub-package stack analysis', async () => { - let expectedSbom = fs.readFileSync(path.join(uvWorkspace, 'packages/sub-pkg/expected_stack_sbom.json')).toString() - expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) - let result = await uvProvider.provideStack(path.join(uvWorkspace, 'packages/sub-pkg/pyproject.toml')) - expect(result).to.deep.equal({ - ecosystem: 'pip', - contentType: 'application/vnd.cyclonedx+json', - content: expectedSbom - }) - }).timeout(TIMEOUT) - - test('verify uv workspace sub-package component analysis', async () => { - let expectedSbom = fs.readFileSync(path.join(uvWorkspace, 'packages/sub-pkg/expected_component_sbom.json')).toString().trim() - expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) - let result = await uvProvider.provideComponent(path.join(uvWorkspace, 'packages/sub-pkg/pyproject.toml')) - expect(result).to.deep.equal({ - ecosystem: 'pip', - contentType: 'application/vnd.cyclonedx+json', - content: expectedSbom - }) - }).timeout(TIMEOUT) + }) }) }).beforeAll(() => clock = useFakeTimers(new Date('2023-10-01T00:00:00.000Z'))).afterAll(() => clock.restore()) diff --git a/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_component_sbom.json b/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_component_sbom.json index 49fc7f43..7d1936f7 100644 --- a/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_component_sbom.json +++ b/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_component_sbom.json @@ -69,4 +69,4 @@ "dependsOn": [] } ] -} \ No newline at end of file +} diff --git a/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_stack_sbom.json b/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_stack_sbom.json index d7a29d9b..a89d1db8 100644 --- a/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_stack_sbom.json +++ b/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_stack_sbom.json @@ -20,6 +20,27 @@ "type": "library", "bom-ref": "pkg:pypi/flask@2.0.3" }, + { + "name": "pywin32", + "version": "311", + "purl": "pkg:pypi/pywin32@311", + "type": "library", + "bom-ref": "pkg:pypi/pywin32@311" + }, + { + "name": "requests", + "version": "2.25.1", + "purl": "pkg:pypi/requests@2.25.1", + "type": "library", + "bom-ref": "pkg:pypi/requests@2.25.1" + }, + { + "name": "typing-extensions", + "version": "4.1.1", + "purl": "pkg:pypi/typing-extensions@4.1.1", + "type": "library", + "bom-ref": "pkg:pypi/typing-extensions@4.1.1" + }, { "name": "click", "version": "8.3.1", @@ -55,20 +76,6 @@ "type": "library", "bom-ref": "pkg:pypi/werkzeug@2.0.3" }, - { - "name": "pywin32", - "version": "311", - "purl": "pkg:pypi/pywin32@311", - "type": "library", - "bom-ref": "pkg:pypi/pywin32@311" - }, - { - "name": "requests", - "version": "2.25.1", - "purl": "pkg:pypi/requests@2.25.1", - "type": "library", - "bom-ref": "pkg:pypi/requests@2.25.1" - }, { "name": "certifi", "version": "2023.7.22", @@ -96,13 +103,6 @@ "purl": "pkg:pypi/urllib3@1.26.16", "type": "library", "bom-ref": "pkg:pypi/urllib3@1.26.16" - }, - { - "name": "typing-extensions", - "version": "4.1.1", - "purl": "pkg:pypi/typing-extensions@4.1.1", - "type": "library", - "bom-ref": "pkg:pypi/typing-extensions@4.1.1" } ], "dependencies": [ @@ -124,6 +124,23 @@ "pkg:pypi/werkzeug@2.0.3" ] }, + { + "ref": "pkg:pypi/pywin32@311", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/requests@2.25.1", + "dependsOn": [ + "pkg:pypi/certifi@2023.7.22", + "pkg:pypi/chardet@4.0.0", + "pkg:pypi/idna@2.10", + "pkg:pypi/urllib3@1.26.16" + ] + }, + { + "ref": "pkg:pypi/typing-extensions@4.1.1", + "dependsOn": [] + }, { "ref": "pkg:pypi/click@8.3.1", "dependsOn": [ @@ -146,19 +163,6 @@ "ref": "pkg:pypi/werkzeug@2.0.3", "dependsOn": [] }, - { - "ref": "pkg:pypi/pywin32@311", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/requests@2.25.1", - "dependsOn": [ - "pkg:pypi/certifi@2023.7.22", - "pkg:pypi/chardet@4.0.0", - "pkg:pypi/idna@2.10", - "pkg:pypi/urllib3@1.26.16" - ] - }, { "ref": "pkg:pypi/certifi@2023.7.22", "dependsOn": [] @@ -174,10 +178,6 @@ { "ref": "pkg:pypi/urllib3@1.26.16", "dependsOn": [] - }, - { - "ref": "pkg:pypi/typing-extensions@4.1.1", - "dependsOn": [] } ] -} \ No newline at end of file +} diff --git a/test/providers/tst_manifests/pyproject/pip_pep621/expected_component_sbom.json b/test/providers/tst_manifests/pyproject/pip_pep621/expected_component_sbom.json new file mode 100644 index 00000000..583a8dff --- /dev/null +++ b/test/providers/tst_manifests/pyproject/pip_pep621/expected_component_sbom.json @@ -0,0 +1,36 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "test-project", + "version": "1.0.0", + "purl": "pkg:pypi/test-project@1.0.0", + "type": "application", + "bom-ref": "pkg:pypi/test-project@1.0.0" + } + }, + "components": [ + { + "name": "requests", + "version": "2.32.3", + "purl": "pkg:pypi/requests@2.32.3", + "type": "library", + "bom-ref": "pkg:pypi/requests@2.32.3" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/test-project@1.0.0", + "dependsOn": [ + "pkg:pypi/requests@2.32.3" + ] + }, + { + "ref": "pkg:pypi/requests@2.32.3", + "dependsOn": [] + } + ] +} diff --git a/test/providers/tst_manifests/pyproject/pip_pep621/expected_stack_sbom.json b/test/providers/tst_manifests/pyproject/pip_pep621/expected_stack_sbom.json new file mode 100644 index 00000000..1d0849c7 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/pip_pep621/expected_stack_sbom.json @@ -0,0 +1,99 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "test-project", + "version": "1.0.0", + "purl": "pkg:pypi/test-project@1.0.0", + "type": "application", + "bom-ref": "pkg:pypi/test-project@1.0.0" + } + }, + "components": [ + { + "name": "requests", + "version": "2.32.3", + "purl": "pkg:pypi/requests@2.32.3", + "type": "library", + "bom-ref": "pkg:pypi/requests@2.32.3" + }, + { + "name": "charset-normalizer", + "version": "3.4.7", + "purl": "pkg:pypi/charset-normalizer@3.4.7", + "type": "library", + "bom-ref": "pkg:pypi/charset-normalizer@3.4.7" + }, + { + "name": "idna", + "version": "3.11", + "purl": "pkg:pypi/idna@3.11", + "type": "library", + "bom-ref": "pkg:pypi/idna@3.11" + }, + { + "name": "urllib3", + "version": "2.6.3", + "purl": "pkg:pypi/urllib3@2.6.3", + "type": "library", + "bom-ref": "pkg:pypi/urllib3@2.6.3" + }, + { + "name": "certifi", + "version": "2026.2.25", + "purl": "pkg:pypi/certifi@2026.2.25", + "type": "library", + "bom-ref": "pkg:pypi/certifi@2026.2.25" + }, + { + "name": "pysocks", + "version": "1.7.1", + "purl": "pkg:pypi/pysocks@1.7.1", + "type": "library", + "bom-ref": "pkg:pypi/pysocks@1.7.1" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/test-project@1.0.0", + "dependsOn": [ + "pkg:pypi/requests@2.32.3" + ] + }, + { + "ref": "pkg:pypi/requests@2.32.3", + "dependsOn": [ + "pkg:pypi/charset-normalizer@3.4.7", + "pkg:pypi/idna@3.11", + "pkg:pypi/urllib3@2.6.3", + "pkg:pypi/certifi@2026.2.25", + "pkg:pypi/pysocks@1.7.1" + ] + }, + { + "ref": "pkg:pypi/charset-normalizer@3.4.7", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/idna@3.11", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/urllib3@2.6.3", + "dependsOn": [ + "pkg:pypi/pysocks@1.7.1" + ] + }, + { + "ref": "pkg:pypi/certifi@2026.2.25", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pysocks@1.7.1", + "dependsOn": [] + } + ] +} diff --git a/test/providers/tst_manifests/pyproject/pip_pep621/pyproject.toml b/test/providers/tst_manifests/pyproject/pip_pep621/pyproject.toml new file mode 100644 index 00000000..14d1ba70 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/pip_pep621/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "test-project" +version = "1.0.0" +requires-python = ">=3.9" +dependencies = [ + "requests[socks]==2.32.3", +] diff --git a/test/providers/tst_manifests/pyproject/pip_pep621_ignore/expected_component_sbom.json b/test/providers/tst_manifests/pyproject/pip_pep621_ignore/expected_component_sbom.json new file mode 100644 index 00000000..36e34eae --- /dev/null +++ b/test/providers/tst_manifests/pyproject/pip_pep621_ignore/expected_component_sbom.json @@ -0,0 +1,17 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "test-project", + "version": "1.0.0", + "purl": "pkg:pypi/test-project@1.0.0", + "type": "application", + "bom-ref": "pkg:pypi/test-project@1.0.0" + } + }, + "components": [], + "dependencies": [] +} diff --git a/test/providers/tst_manifests/pyproject/pip_pep621_ignore/expected_stack_sbom.json b/test/providers/tst_manifests/pyproject/pip_pep621_ignore/expected_stack_sbom.json new file mode 100644 index 00000000..36e34eae --- /dev/null +++ b/test/providers/tst_manifests/pyproject/pip_pep621_ignore/expected_stack_sbom.json @@ -0,0 +1,17 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "test-project", + "version": "1.0.0", + "purl": "pkg:pypi/test-project@1.0.0", + "type": "application", + "bom-ref": "pkg:pypi/test-project@1.0.0" + } + }, + "components": [], + "dependencies": [] +} diff --git a/test/providers/tst_manifests/pyproject/pip_pep621_ignore/pyproject.toml b/test/providers/tst_manifests/pyproject/pip_pep621_ignore/pyproject.toml new file mode 100644 index 00000000..3c315ee3 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/pip_pep621_ignore/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "test-project" +version = "1.0.0" +requires-python = ">=3.9" +dependencies = [ + "requests==2.32.3", #exhortignore +] diff --git a/test/providers/tst_manifests/pyproject/poetry_lock/expected_component_sbom.json b/test/providers/tst_manifests/pyproject/poetry_lock/expected_component_sbom.json index 846a9746..990ca08f 100644 --- a/test/providers/tst_manifests/pyproject/poetry_lock/expected_component_sbom.json +++ b/test/providers/tst_manifests/pyproject/poetry_lock/expected_component_sbom.json @@ -45,4 +45,4 @@ "dependsOn": [] } ] -} \ No newline at end of file +} diff --git a/test/providers/tst_manifests/pyproject/poetry_lock/expected_stack_sbom.json b/test/providers/tst_manifests/pyproject/poetry_lock/expected_stack_sbom.json index a70f75a5..bac66b4b 100644 --- a/test/providers/tst_manifests/pyproject/poetry_lock/expected_stack_sbom.json +++ b/test/providers/tst_manifests/pyproject/poetry_lock/expected_stack_sbom.json @@ -20,6 +20,13 @@ "type": "library", "bom-ref": "pkg:pypi/flask@3.1.3" }, + { + "name": "requests", + "version": "2.33.0", + "purl": "pkg:pypi/requests@2.33.0", + "type": "library", + "bom-ref": "pkg:pypi/requests@2.33.0" + }, { "name": "blinker", "version": "1.9.0", @@ -55,13 +62,6 @@ "type": "library", "bom-ref": "pkg:pypi/werkzeug@3.1.7" }, - { - "name": "requests", - "version": "2.33.0", - "purl": "pkg:pypi/requests@2.33.0", - "type": "library", - "bom-ref": "pkg:pypi/requests@2.33.0" - }, { "name": "certifi", "version": "2026.2.25", @@ -105,9 +105,19 @@ "pkg:pypi/blinker@1.9.0", "pkg:pypi/itsdangerous@2.2.0", "pkg:pypi/jinja2@3.1.6", + "pkg:pypi/markupsafe@3.0.3", "pkg:pypi/werkzeug@3.1.7" ] }, + { + "ref": "pkg:pypi/requests@2.33.0", + "dependsOn": [ + "pkg:pypi/certifi@2026.2.25", + "pkg:pypi/charset-normalizer@3.4.6", + "pkg:pypi/idna@3.11", + "pkg:pypi/urllib3@2.6.3" + ] + }, { "ref": "pkg:pypi/blinker@1.9.0", "dependsOn": [] @@ -128,15 +138,8 @@ }, { "ref": "pkg:pypi/werkzeug@3.1.7", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/requests@2.33.0", "dependsOn": [ - "pkg:pypi/certifi@2026.2.25", - "pkg:pypi/charset-normalizer@3.4.6", - "pkg:pypi/idna@3.11", - "pkg:pypi/urllib3@2.6.3" + "pkg:pypi/markupsafe@3.0.3" ] }, { @@ -156,4 +159,4 @@ "dependsOn": [] } ] -} \ No newline at end of file +} diff --git a/test/providers/tst_manifests/pyproject/poetry_only_deps/expected_stack_sbom.json b/test/providers/tst_manifests/pyproject/poetry_only_deps/expected_stack_sbom.json index 2f7167af..31d20f56 100644 --- a/test/providers/tst_manifests/pyproject/poetry_only_deps/expected_stack_sbom.json +++ b/test/providers/tst_manifests/pyproject/poetry_only_deps/expected_stack_sbom.json @@ -20,13 +20,6 @@ "type": "library", "bom-ref": "pkg:pypi/click@8.1.8" }, - { - "name": "colorama", - "version": "0.4.6", - "purl": "pkg:pypi/colorama@0.4.6", - "type": "library", - "bom-ref": "pkg:pypi/colorama@0.4.6" - }, { "name": "flask", "version": "2.3.3", @@ -34,6 +27,20 @@ "type": "library", "bom-ref": "pkg:pypi/flask@2.3.3" }, + { + "name": "requests", + "version": "2.32.4", + "purl": "pkg:pypi/requests@2.32.4", + "type": "library", + "bom-ref": "pkg:pypi/requests@2.32.4" + }, + { + "name": "colorama", + "version": "0.4.6", + "purl": "pkg:pypi/colorama@0.4.6", + "type": "library", + "bom-ref": "pkg:pypi/colorama@0.4.6" + }, { "name": "blinker", "version": "1.8.2", @@ -48,13 +55,6 @@ "type": "library", "bom-ref": "pkg:pypi/importlib-metadata@8.5.0" }, - { - "name": "zipp", - "version": "3.20.2", - "purl": "pkg:pypi/zipp@3.20.2", - "type": "library", - "bom-ref": "pkg:pypi/zipp@3.20.2" - }, { "name": "itsdangerous", "version": "2.2.0", @@ -69,13 +69,6 @@ "type": "library", "bom-ref": "pkg:pypi/jinja2@3.1.6" }, - { - "name": "markupsafe", - "version": "2.1.5", - "purl": "pkg:pypi/markupsafe@2.1.5", - "type": "library", - "bom-ref": "pkg:pypi/markupsafe@2.1.5" - }, { "name": "werkzeug", "version": "3.0.6", @@ -84,11 +77,18 @@ "bom-ref": "pkg:pypi/werkzeug@3.0.6" }, { - "name": "requests", - "version": "2.32.4", - "purl": "pkg:pypi/requests@2.32.4", + "name": "zipp", + "version": "3.20.2", + "purl": "pkg:pypi/zipp@3.20.2", "type": "library", - "bom-ref": "pkg:pypi/requests@2.32.4" + "bom-ref": "pkg:pypi/zipp@3.20.2" + }, + { + "name": "markupsafe", + "version": "2.1.5", + "purl": "pkg:pypi/markupsafe@2.1.5", + "type": "library", + "bom-ref": "pkg:pypi/markupsafe@2.1.5" }, { "name": "certifi", @@ -141,10 +141,6 @@ "pkg:pypi/colorama@0.4.6" ] }, - { - "ref": "pkg:pypi/colorama@0.4.6", - "dependsOn": [] - }, { "ref": "pkg:pypi/flask@2.3.3", "dependsOn": [ @@ -156,6 +152,20 @@ "pkg:pypi/werkzeug@3.0.6" ] }, + { + "ref": "pkg:pypi/requests@2.32.4", + "dependsOn": [ + "pkg:pypi/certifi@2026.2.25", + "pkg:pypi/charset-normalizer@3.4.6", + "pkg:pypi/idna@3.11", + "pkg:pypi/pysocks@1.7.1", + "pkg:pypi/urllib3@2.2.3" + ] + }, + { + "ref": "pkg:pypi/colorama@0.4.6", + "dependsOn": [] + }, { "ref": "pkg:pypi/blinker@1.8.2", "dependsOn": [] @@ -166,10 +176,6 @@ "pkg:pypi/zipp@3.20.2" ] }, - { - "ref": "pkg:pypi/zipp@3.20.2", - "dependsOn": [] - }, { "ref": "pkg:pypi/itsdangerous@2.2.0", "dependsOn": [] @@ -181,22 +187,18 @@ ] }, { - "ref": "pkg:pypi/markupsafe@2.1.5", - "dependsOn": [] + "ref": "pkg:pypi/werkzeug@3.0.6", + "dependsOn": [ + "pkg:pypi/markupsafe@2.1.5" + ] }, { - "ref": "pkg:pypi/werkzeug@3.0.6", + "ref": "pkg:pypi/zipp@3.20.2", "dependsOn": [] }, { - "ref": "pkg:pypi/requests@2.32.4", - "dependsOn": [ - "pkg:pypi/certifi@2026.2.25", - "pkg:pypi/charset-normalizer@3.4.6", - "pkg:pypi/idna@3.11", - "pkg:pypi/pysocks@1.7.1", - "pkg:pypi/urllib3@2.2.3" - ] + "ref": "pkg:pypi/markupsafe@2.1.5", + "dependsOn": [] }, { "ref": "pkg:pypi/certifi@2026.2.25", diff --git a/test/providers/tst_manifests/pyproject/uv_lock/expected_stack_sbom.json b/test/providers/tst_manifests/pyproject/uv_lock/expected_stack_sbom.json index 6d528dd8..9f90d870 100644 --- a/test/providers/tst_manifests/pyproject/uv_lock/expected_stack_sbom.json +++ b/test/providers/tst_manifests/pyproject/uv_lock/expected_stack_sbom.json @@ -20,6 +20,13 @@ "type": "library", "bom-ref": "pkg:pypi/flask@2.0.3" }, + { + "name": "requests", + "version": "2.25.1", + "purl": "pkg:pypi/requests@2.25.1", + "type": "library", + "bom-ref": "pkg:pypi/requests@2.25.1" + }, { "name": "click", "version": "8.3.1", @@ -48,13 +55,6 @@ "type": "library", "bom-ref": "pkg:pypi/jinja2@3.0.3" }, - { - "name": "markupsafe", - "version": "2.0.1", - "purl": "pkg:pypi/markupsafe@2.0.1", - "type": "library", - "bom-ref": "pkg:pypi/markupsafe@2.0.1" - }, { "name": "werkzeug", "version": "2.0.3", @@ -63,11 +63,11 @@ "bom-ref": "pkg:pypi/werkzeug@2.0.3" }, { - "name": "requests", - "version": "2.25.1", - "purl": "pkg:pypi/requests@2.25.1", + "name": "markupsafe", + "version": "2.0.1", + "purl": "pkg:pypi/markupsafe@2.0.1", "type": "library", - "bom-ref": "pkg:pypi/requests@2.25.1" + "bom-ref": "pkg:pypi/markupsafe@2.0.1" }, { "name": "certifi", @@ -115,6 +115,15 @@ "pkg:pypi/werkzeug@2.0.3" ] }, + { + "ref": "pkg:pypi/requests@2.25.1", + "dependsOn": [ + "pkg:pypi/certifi@2023.7.22", + "pkg:pypi/chardet@4.0.0", + "pkg:pypi/idna@2.10", + "pkg:pypi/urllib3@1.26.16" + ] + }, { "ref": "pkg:pypi/click@8.3.1", "dependsOn": [ @@ -135,22 +144,13 @@ "pkg:pypi/markupsafe@2.0.1" ] }, - { - "ref": "pkg:pypi/markupsafe@2.0.1", - "dependsOn": [] - }, { "ref": "pkg:pypi/werkzeug@2.0.3", "dependsOn": [] }, { - "ref": "pkg:pypi/requests@2.25.1", - "dependsOn": [ - "pkg:pypi/certifi@2023.7.22", - "pkg:pypi/chardet@4.0.0", - "pkg:pypi/idna@2.10", - "pkg:pypi/urllib3@1.26.16" - ] + "ref": "pkg:pypi/markupsafe@2.0.1", + "dependsOn": [] }, { "ref": "pkg:pypi/certifi@2023.7.22", diff --git a/test/providers/tst_manifests/pyproject/uv_workspace/expected_component_sbom.json b/test/providers/tst_manifests/pyproject/uv_workspace/expected_component_sbom.json index 9e09c503..625332dc 100644 --- a/test/providers/tst_manifests/pyproject/uv_workspace/expected_component_sbom.json +++ b/test/providers/tst_manifests/pyproject/uv_workspace/expected_component_sbom.json @@ -1,48 +1,48 @@ { - "bomFormat": "CycloneDX", - "specVersion": "1.4", - "version": 1, - "metadata": { - "timestamp": "2023-10-01T00:00:00.000Z", - "component": { - "name": "uv-mono", - "version": "0.1.0", - "purl": "pkg:pypi/uv-mono@0.1.0", - "type": "application", - "bom-ref": "pkg:pypi/uv-mono@0.1.0" - } - }, - "components": [ - { - "name": "flask", - "version": "2.0.3", - "purl": "pkg:pypi/flask@2.0.3", - "type": "library", - "bom-ref": "pkg:pypi/flask@2.0.3" - }, - { - "name": "mid-pkg", - "version": "0.1.0", - "purl": "pkg:pypi/mid-pkg@0.1.0", - "type": "library", - "bom-ref": "pkg:pypi/mid-pkg@0.1.0" - } - ], - "dependencies": [ - { - "ref": "pkg:pypi/uv-mono@0.1.0", - "dependsOn": [ - "pkg:pypi/flask@2.0.3", - "pkg:pypi/mid-pkg@0.1.0" - ] - }, - { - "ref": "pkg:pypi/flask@2.0.3", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/mid-pkg@0.1.0", - "dependsOn": [] - } - ] + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "uv-mono", + "version": "0.1.0", + "purl": "pkg:pypi/uv-mono@0.1.0", + "type": "application", + "bom-ref": "pkg:pypi/uv-mono@0.1.0" + } + }, + "components": [ + { + "name": "mid-pkg", + "version": "0.1.0", + "purl": "pkg:pypi/mid-pkg@0.1.0", + "type": "library", + "bom-ref": "pkg:pypi/mid-pkg@0.1.0" + }, + { + "name": "flask", + "version": "2.0.3", + "purl": "pkg:pypi/flask@2.0.3", + "type": "library", + "bom-ref": "pkg:pypi/flask@2.0.3" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/uv-mono@0.1.0", + "dependsOn": [ + "pkg:pypi/mid-pkg@0.1.0", + "pkg:pypi/flask@2.0.3" + ] + }, + { + "ref": "pkg:pypi/mid-pkg@0.1.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/flask@2.0.3", + "dependsOn": [] + } + ] } diff --git a/test/providers/tst_manifests/pyproject/uv_workspace/expected_stack_sbom.json b/test/providers/tst_manifests/pyproject/uv_workspace/expected_stack_sbom.json index 77df939e..c4b77af6 100644 --- a/test/providers/tst_manifests/pyproject/uv_workspace/expected_stack_sbom.json +++ b/test/providers/tst_manifests/pyproject/uv_workspace/expected_stack_sbom.json @@ -1,198 +1,200 @@ { - "bomFormat": "CycloneDX", - "specVersion": "1.4", - "version": 1, - "metadata": { - "timestamp": "2023-10-01T00:00:00.000Z", - "component": { - "name": "uv-mono", - "version": "0.1.0", - "purl": "pkg:pypi/uv-mono@0.1.0", - "type": "application", - "bom-ref": "pkg:pypi/uv-mono@0.1.0" - } - }, - "components": [ - { - "name": "flask", - "version": "2.0.3", - "purl": "pkg:pypi/flask@2.0.3", - "type": "library", - "bom-ref": "pkg:pypi/flask@2.0.3" - }, - { - "name": "click", - "version": "8.3.2", - "purl": "pkg:pypi/click@8.3.2", - "type": "library", - "bom-ref": "pkg:pypi/click@8.3.2" - }, - { - "name": "colorama", - "version": "0.4.6", - "purl": "pkg:pypi/colorama@0.4.6", - "type": "library", - "bom-ref": "pkg:pypi/colorama@0.4.6" - }, - { - "name": "itsdangerous", - "version": "2.2.0", - "purl": "pkg:pypi/itsdangerous@2.2.0", - "type": "library", - "bom-ref": "pkg:pypi/itsdangerous@2.2.0" - }, - { - "name": "jinja2", - "version": "3.1.6", - "purl": "pkg:pypi/jinja2@3.1.6", - "type": "library", - "bom-ref": "pkg:pypi/jinja2@3.1.6" - }, - { - "name": "markupsafe", - "version": "3.0.3", - "purl": "pkg:pypi/markupsafe@3.0.3", - "type": "library", - "bom-ref": "pkg:pypi/markupsafe@3.0.3" - }, - { - "name": "werkzeug", - "version": "3.1.8", - "purl": "pkg:pypi/werkzeug@3.1.8", - "type": "library", - "bom-ref": "pkg:pypi/werkzeug@3.1.8" - }, - { - "name": "mid-pkg", - "version": "0.1.0", - "purl": "pkg:pypi/mid-pkg@0.1.0", - "type": "library", - "bom-ref": "pkg:pypi/mid-pkg@0.1.0" - }, - { - "name": "sub-pkg", - "version": "0.1.0", - "purl": "pkg:pypi/sub-pkg@0.1.0", - "type": "library", - "bom-ref": "pkg:pypi/sub-pkg@0.1.0" - }, - { - "name": "requests", - "version": "2.33.1", - "purl": "pkg:pypi/requests@2.33.1", - "type": "library", - "bom-ref": "pkg:pypi/requests@2.33.1" - }, - { - "name": "certifi", - "version": "2026.2.25", - "purl": "pkg:pypi/certifi@2026.2.25", - "type": "library", - "bom-ref": "pkg:pypi/certifi@2026.2.25" - }, - { - "name": "charset-normalizer", - "version": "3.4.7", - "purl": "pkg:pypi/charset-normalizer@3.4.7", - "type": "library", - "bom-ref": "pkg:pypi/charset-normalizer@3.4.7" - }, - { - "name": "idna", - "version": "3.11", - "purl": "pkg:pypi/idna@3.11", - "type": "library", - "bom-ref": "pkg:pypi/idna@3.11" - }, - { - "name": "urllib3", - "version": "2.6.3", - "purl": "pkg:pypi/urllib3@2.6.3", - "type": "library", - "bom-ref": "pkg:pypi/urllib3@2.6.3" - } - ], - "dependencies": [ - { - "ref": "pkg:pypi/uv-mono@0.1.0", - "dependsOn": [ - "pkg:pypi/flask@2.0.3", - "pkg:pypi/mid-pkg@0.1.0" - ] - }, - { - "ref": "pkg:pypi/flask@2.0.3", - "dependsOn": [ - "pkg:pypi/click@8.3.2", - "pkg:pypi/itsdangerous@2.2.0", - "pkg:pypi/jinja2@3.1.6", - "pkg:pypi/werkzeug@3.1.8" - ] - }, - { - "ref": "pkg:pypi/click@8.3.2", - "dependsOn": [ - "pkg:pypi/colorama@0.4.6" - ] - }, - { - "ref": "pkg:pypi/colorama@0.4.6", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/itsdangerous@2.2.0", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/jinja2@3.1.6", - "dependsOn": [ - "pkg:pypi/markupsafe@3.0.3" - ] - }, - { - "ref": "pkg:pypi/markupsafe@3.0.3", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/werkzeug@3.1.8", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/mid-pkg@0.1.0", - "dependsOn": [ - "pkg:pypi/sub-pkg@0.1.0" - ] - }, - { - "ref": "pkg:pypi/sub-pkg@0.1.0", - "dependsOn": [ - "pkg:pypi/requests@2.33.1" - ] - }, - { - "ref": "pkg:pypi/requests@2.33.1", - "dependsOn": [ - "pkg:pypi/certifi@2026.2.25", - "pkg:pypi/charset-normalizer@3.4.7", - "pkg:pypi/idna@3.11", - "pkg:pypi/urllib3@2.6.3" - ] - }, - { - "ref": "pkg:pypi/certifi@2026.2.25", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/charset-normalizer@3.4.7", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/idna@3.11", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/urllib3@2.6.3", - "dependsOn": [] - } - ] + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "uv-mono", + "version": "0.1.0", + "purl": "pkg:pypi/uv-mono@0.1.0", + "type": "application", + "bom-ref": "pkg:pypi/uv-mono@0.1.0" + } + }, + "components": [ + { + "name": "mid-pkg", + "version": "0.1.0", + "purl": "pkg:pypi/mid-pkg@0.1.0", + "type": "library", + "bom-ref": "pkg:pypi/mid-pkg@0.1.0" + }, + { + "name": "flask", + "version": "2.0.3", + "purl": "pkg:pypi/flask@2.0.3", + "type": "library", + "bom-ref": "pkg:pypi/flask@2.0.3" + }, + { + "name": "sub-pkg", + "version": "0.1.0", + "purl": "pkg:pypi/sub-pkg@0.1.0", + "type": "library", + "bom-ref": "pkg:pypi/sub-pkg@0.1.0" + }, + { + "name": "requests", + "version": "2.33.1", + "purl": "pkg:pypi/requests@2.33.1", + "type": "library", + "bom-ref": "pkg:pypi/requests@2.33.1" + }, + { + "name": "click", + "version": "8.3.2", + "purl": "pkg:pypi/click@8.3.2", + "type": "library", + "bom-ref": "pkg:pypi/click@8.3.2" + }, + { + "name": "colorama", + "version": "0.4.6", + "purl": "pkg:pypi/colorama@0.4.6", + "type": "library", + "bom-ref": "pkg:pypi/colorama@0.4.6" + }, + { + "name": "itsdangerous", + "version": "2.2.0", + "purl": "pkg:pypi/itsdangerous@2.2.0", + "type": "library", + "bom-ref": "pkg:pypi/itsdangerous@2.2.0" + }, + { + "name": "jinja2", + "version": "3.1.6", + "purl": "pkg:pypi/jinja2@3.1.6", + "type": "library", + "bom-ref": "pkg:pypi/jinja2@3.1.6" + }, + { + "name": "werkzeug", + "version": "3.1.8", + "purl": "pkg:pypi/werkzeug@3.1.8", + "type": "library", + "bom-ref": "pkg:pypi/werkzeug@3.1.8" + }, + { + "name": "markupsafe", + "version": "3.0.3", + "purl": "pkg:pypi/markupsafe@3.0.3", + "type": "library", + "bom-ref": "pkg:pypi/markupsafe@3.0.3" + }, + { + "name": "certifi", + "version": "2026.2.25", + "purl": "pkg:pypi/certifi@2026.2.25", + "type": "library", + "bom-ref": "pkg:pypi/certifi@2026.2.25" + }, + { + "name": "charset-normalizer", + "version": "3.4.7", + "purl": "pkg:pypi/charset-normalizer@3.4.7", + "type": "library", + "bom-ref": "pkg:pypi/charset-normalizer@3.4.7" + }, + { + "name": "idna", + "version": "3.11", + "purl": "pkg:pypi/idna@3.11", + "type": "library", + "bom-ref": "pkg:pypi/idna@3.11" + }, + { + "name": "urllib3", + "version": "2.6.3", + "purl": "pkg:pypi/urllib3@2.6.3", + "type": "library", + "bom-ref": "pkg:pypi/urllib3@2.6.3" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/uv-mono@0.1.0", + "dependsOn": [ + "pkg:pypi/mid-pkg@0.1.0", + "pkg:pypi/flask@2.0.3" + ] + }, + { + "ref": "pkg:pypi/mid-pkg@0.1.0", + "dependsOn": [ + "pkg:pypi/sub-pkg@0.1.0" + ] + }, + { + "ref": "pkg:pypi/flask@2.0.3", + "dependsOn": [ + "pkg:pypi/click@8.3.2", + "pkg:pypi/itsdangerous@2.2.0", + "pkg:pypi/jinja2@3.1.6", + "pkg:pypi/werkzeug@3.1.8" + ] + }, + { + "ref": "pkg:pypi/sub-pkg@0.1.0", + "dependsOn": [ + "pkg:pypi/requests@2.33.1" + ] + }, + { + "ref": "pkg:pypi/requests@2.33.1", + "dependsOn": [ + "pkg:pypi/certifi@2026.2.25", + "pkg:pypi/charset-normalizer@3.4.7", + "pkg:pypi/idna@3.11", + "pkg:pypi/urllib3@2.6.3" + ] + }, + { + "ref": "pkg:pypi/click@8.3.2", + "dependsOn": [ + "pkg:pypi/colorama@0.4.6" + ] + }, + { + "ref": "pkg:pypi/colorama@0.4.6", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/itsdangerous@2.2.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/jinja2@3.1.6", + "dependsOn": [ + "pkg:pypi/markupsafe@3.0.3" + ] + }, + { + "ref": "pkg:pypi/werkzeug@3.1.8", + "dependsOn": [ + "pkg:pypi/markupsafe@3.0.3" + ] + }, + { + "ref": "pkg:pypi/markupsafe@3.0.3", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/certifi@2026.2.25", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/charset-normalizer@3.4.7", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/idna@3.11", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/urllib3@2.6.3", + "dependsOn": [] + } + ] } diff --git a/test/providers/tst_manifests/pyproject/uv_workspace/packages/mid-pkg/expected_component_sbom.json b/test/providers/tst_manifests/pyproject/uv_workspace/packages/mid-pkg/expected_component_sbom.json index 6028991b..149a7c8c 100644 --- a/test/providers/tst_manifests/pyproject/uv_workspace/packages/mid-pkg/expected_component_sbom.json +++ b/test/providers/tst_manifests/pyproject/uv_workspace/packages/mid-pkg/expected_component_sbom.json @@ -1,36 +1,36 @@ { - "bomFormat": "CycloneDX", - "specVersion": "1.4", - "version": 1, - "metadata": { - "timestamp": "2023-10-01T00:00:00.000Z", - "component": { - "name": "mid-pkg", - "version": "0.1.0", - "purl": "pkg:pypi/mid-pkg@0.1.0", - "type": "application", - "bom-ref": "pkg:pypi/mid-pkg@0.1.0" - } - }, - "components": [ - { - "name": "sub-pkg", - "version": "0.1.0", - "purl": "pkg:pypi/sub-pkg@0.1.0", - "type": "library", - "bom-ref": "pkg:pypi/sub-pkg@0.1.0" - } - ], - "dependencies": [ - { - "ref": "pkg:pypi/mid-pkg@0.1.0", - "dependsOn": [ - "pkg:pypi/sub-pkg@0.1.0" - ] - }, - { - "ref": "pkg:pypi/sub-pkg@0.1.0", - "dependsOn": [] - } - ] + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "mid-pkg", + "version": "0.1.0", + "purl": "pkg:pypi/mid-pkg@0.1.0", + "type": "application", + "bom-ref": "pkg:pypi/mid-pkg@0.1.0" + } + }, + "components": [ + { + "name": "sub-pkg", + "version": "0.1.0", + "purl": "pkg:pypi/sub-pkg@0.1.0", + "type": "library", + "bom-ref": "pkg:pypi/sub-pkg@0.1.0" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/mid-pkg@0.1.0", + "dependsOn": [ + "pkg:pypi/sub-pkg@0.1.0" + ] + }, + { + "ref": "pkg:pypi/sub-pkg@0.1.0", + "dependsOn": [] + } + ] } diff --git a/test/providers/tst_manifests/pyproject/uv_workspace/packages/mid-pkg/expected_stack_sbom.json b/test/providers/tst_manifests/pyproject/uv_workspace/packages/mid-pkg/expected_stack_sbom.json index 506c9dca..0c72587b 100644 --- a/test/providers/tst_manifests/pyproject/uv_workspace/packages/mid-pkg/expected_stack_sbom.json +++ b/test/providers/tst_manifests/pyproject/uv_workspace/packages/mid-pkg/expected_stack_sbom.json @@ -1,98 +1,98 @@ { - "bomFormat": "CycloneDX", - "specVersion": "1.4", - "version": 1, - "metadata": { - "timestamp": "2023-10-01T00:00:00.000Z", - "component": { - "name": "mid-pkg", - "version": "0.1.0", - "purl": "pkg:pypi/mid-pkg@0.1.0", - "type": "application", - "bom-ref": "pkg:pypi/mid-pkg@0.1.0" - } - }, - "components": [ - { - "name": "sub-pkg", - "version": "0.1.0", - "purl": "pkg:pypi/sub-pkg@0.1.0", - "type": "library", - "bom-ref": "pkg:pypi/sub-pkg@0.1.0" - }, - { - "name": "requests", - "version": "2.33.1", - "purl": "pkg:pypi/requests@2.33.1", - "type": "library", - "bom-ref": "pkg:pypi/requests@2.33.1" - }, - { - "name": "certifi", - "version": "2026.2.25", - "purl": "pkg:pypi/certifi@2026.2.25", - "type": "library", - "bom-ref": "pkg:pypi/certifi@2026.2.25" - }, - { - "name": "charset-normalizer", - "version": "3.4.7", - "purl": "pkg:pypi/charset-normalizer@3.4.7", - "type": "library", - "bom-ref": "pkg:pypi/charset-normalizer@3.4.7" - }, - { - "name": "idna", - "version": "3.11", - "purl": "pkg:pypi/idna@3.11", - "type": "library", - "bom-ref": "pkg:pypi/idna@3.11" - }, - { - "name": "urllib3", - "version": "2.6.3", - "purl": "pkg:pypi/urllib3@2.6.3", - "type": "library", - "bom-ref": "pkg:pypi/urllib3@2.6.3" - } - ], - "dependencies": [ - { - "ref": "pkg:pypi/mid-pkg@0.1.0", - "dependsOn": [ - "pkg:pypi/sub-pkg@0.1.0" - ] - }, - { - "ref": "pkg:pypi/sub-pkg@0.1.0", - "dependsOn": [ - "pkg:pypi/requests@2.33.1" - ] - }, - { - "ref": "pkg:pypi/requests@2.33.1", - "dependsOn": [ - "pkg:pypi/certifi@2026.2.25", - "pkg:pypi/charset-normalizer@3.4.7", - "pkg:pypi/idna@3.11", - "pkg:pypi/urllib3@2.6.3" - ] - }, - { - "ref": "pkg:pypi/certifi@2026.2.25", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/charset-normalizer@3.4.7", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/idna@3.11", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/urllib3@2.6.3", - "dependsOn": [] - } - ] + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "mid-pkg", + "version": "0.1.0", + "purl": "pkg:pypi/mid-pkg@0.1.0", + "type": "application", + "bom-ref": "pkg:pypi/mid-pkg@0.1.0" + } + }, + "components": [ + { + "name": "sub-pkg", + "version": "0.1.0", + "purl": "pkg:pypi/sub-pkg@0.1.0", + "type": "library", + "bom-ref": "pkg:pypi/sub-pkg@0.1.0" + }, + { + "name": "requests", + "version": "2.33.1", + "purl": "pkg:pypi/requests@2.33.1", + "type": "library", + "bom-ref": "pkg:pypi/requests@2.33.1" + }, + { + "name": "certifi", + "version": "2026.2.25", + "purl": "pkg:pypi/certifi@2026.2.25", + "type": "library", + "bom-ref": "pkg:pypi/certifi@2026.2.25" + }, + { + "name": "charset-normalizer", + "version": "3.4.7", + "purl": "pkg:pypi/charset-normalizer@3.4.7", + "type": "library", + "bom-ref": "pkg:pypi/charset-normalizer@3.4.7" + }, + { + "name": "idna", + "version": "3.11", + "purl": "pkg:pypi/idna@3.11", + "type": "library", + "bom-ref": "pkg:pypi/idna@3.11" + }, + { + "name": "urllib3", + "version": "2.6.3", + "purl": "pkg:pypi/urllib3@2.6.3", + "type": "library", + "bom-ref": "pkg:pypi/urllib3@2.6.3" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/mid-pkg@0.1.0", + "dependsOn": [ + "pkg:pypi/sub-pkg@0.1.0" + ] + }, + { + "ref": "pkg:pypi/sub-pkg@0.1.0", + "dependsOn": [ + "pkg:pypi/requests@2.33.1" + ] + }, + { + "ref": "pkg:pypi/requests@2.33.1", + "dependsOn": [ + "pkg:pypi/certifi@2026.2.25", + "pkg:pypi/charset-normalizer@3.4.7", + "pkg:pypi/idna@3.11", + "pkg:pypi/urllib3@2.6.3" + ] + }, + { + "ref": "pkg:pypi/certifi@2026.2.25", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/charset-normalizer@3.4.7", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/idna@3.11", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/urllib3@2.6.3", + "dependsOn": [] + } + ] } diff --git a/test/providers/tst_manifests/pyproject/uv_workspace/packages/sub-pkg/expected_component_sbom.json b/test/providers/tst_manifests/pyproject/uv_workspace/packages/sub-pkg/expected_component_sbom.json index dec8c565..6c3f39a5 100644 --- a/test/providers/tst_manifests/pyproject/uv_workspace/packages/sub-pkg/expected_component_sbom.json +++ b/test/providers/tst_manifests/pyproject/uv_workspace/packages/sub-pkg/expected_component_sbom.json @@ -1,36 +1,36 @@ { - "bomFormat": "CycloneDX", - "specVersion": "1.4", - "version": 1, - "metadata": { - "timestamp": "2023-10-01T00:00:00.000Z", - "component": { - "name": "sub-pkg", - "version": "0.1.0", - "purl": "pkg:pypi/sub-pkg@0.1.0", - "type": "application", - "bom-ref": "pkg:pypi/sub-pkg@0.1.0" - } - }, - "components": [ - { - "name": "requests", - "version": "2.33.1", - "purl": "pkg:pypi/requests@2.33.1", - "type": "library", - "bom-ref": "pkg:pypi/requests@2.33.1" - } - ], - "dependencies": [ - { - "ref": "pkg:pypi/sub-pkg@0.1.0", - "dependsOn": [ - "pkg:pypi/requests@2.33.1" - ] - }, - { - "ref": "pkg:pypi/requests@2.33.1", - "dependsOn": [] - } - ] + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "sub-pkg", + "version": "0.1.0", + "purl": "pkg:pypi/sub-pkg@0.1.0", + "type": "application", + "bom-ref": "pkg:pypi/sub-pkg@0.1.0" + } + }, + "components": [ + { + "name": "requests", + "version": "2.33.1", + "purl": "pkg:pypi/requests@2.33.1", + "type": "library", + "bom-ref": "pkg:pypi/requests@2.33.1" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/sub-pkg@0.1.0", + "dependsOn": [ + "pkg:pypi/requests@2.33.1" + ] + }, + { + "ref": "pkg:pypi/requests@2.33.1", + "dependsOn": [] + } + ] } diff --git a/test/providers/tst_manifests/pyproject/uv_workspace/packages/sub-pkg/expected_stack_sbom.json b/test/providers/tst_manifests/pyproject/uv_workspace/packages/sub-pkg/expected_stack_sbom.json index e9d1f86d..62e86b30 100644 --- a/test/providers/tst_manifests/pyproject/uv_workspace/packages/sub-pkg/expected_stack_sbom.json +++ b/test/providers/tst_manifests/pyproject/uv_workspace/packages/sub-pkg/expected_stack_sbom.json @@ -1,85 +1,85 @@ { - "bomFormat": "CycloneDX", - "specVersion": "1.4", - "version": 1, - "metadata": { - "timestamp": "2023-10-01T00:00:00.000Z", - "component": { - "name": "sub-pkg", - "version": "0.1.0", - "purl": "pkg:pypi/sub-pkg@0.1.0", - "type": "application", - "bom-ref": "pkg:pypi/sub-pkg@0.1.0" - } - }, - "components": [ - { - "name": "requests", - "version": "2.33.1", - "purl": "pkg:pypi/requests@2.33.1", - "type": "library", - "bom-ref": "pkg:pypi/requests@2.33.1" - }, - { - "name": "certifi", - "version": "2026.2.25", - "purl": "pkg:pypi/certifi@2026.2.25", - "type": "library", - "bom-ref": "pkg:pypi/certifi@2026.2.25" - }, - { - "name": "charset-normalizer", - "version": "3.4.7", - "purl": "pkg:pypi/charset-normalizer@3.4.7", - "type": "library", - "bom-ref": "pkg:pypi/charset-normalizer@3.4.7" - }, - { - "name": "idna", - "version": "3.11", - "purl": "pkg:pypi/idna@3.11", - "type": "library", - "bom-ref": "pkg:pypi/idna@3.11" - }, - { - "name": "urllib3", - "version": "2.6.3", - "purl": "pkg:pypi/urllib3@2.6.3", - "type": "library", - "bom-ref": "pkg:pypi/urllib3@2.6.3" - } - ], - "dependencies": [ - { - "ref": "pkg:pypi/sub-pkg@0.1.0", - "dependsOn": [ - "pkg:pypi/requests@2.33.1" - ] - }, - { - "ref": "pkg:pypi/requests@2.33.1", - "dependsOn": [ - "pkg:pypi/certifi@2026.2.25", - "pkg:pypi/charset-normalizer@3.4.7", - "pkg:pypi/idna@3.11", - "pkg:pypi/urllib3@2.6.3" - ] - }, - { - "ref": "pkg:pypi/certifi@2026.2.25", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/charset-normalizer@3.4.7", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/idna@3.11", - "dependsOn": [] - }, - { - "ref": "pkg:pypi/urllib3@2.6.3", - "dependsOn": [] - } - ] + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "sub-pkg", + "version": "0.1.0", + "purl": "pkg:pypi/sub-pkg@0.1.0", + "type": "application", + "bom-ref": "pkg:pypi/sub-pkg@0.1.0" + } + }, + "components": [ + { + "name": "requests", + "version": "2.33.1", + "purl": "pkg:pypi/requests@2.33.1", + "type": "library", + "bom-ref": "pkg:pypi/requests@2.33.1" + }, + { + "name": "certifi", + "version": "2026.2.25", + "purl": "pkg:pypi/certifi@2026.2.25", + "type": "library", + "bom-ref": "pkg:pypi/certifi@2026.2.25" + }, + { + "name": "charset-normalizer", + "version": "3.4.7", + "purl": "pkg:pypi/charset-normalizer@3.4.7", + "type": "library", + "bom-ref": "pkg:pypi/charset-normalizer@3.4.7" + }, + { + "name": "idna", + "version": "3.11", + "purl": "pkg:pypi/idna@3.11", + "type": "library", + "bom-ref": "pkg:pypi/idna@3.11" + }, + { + "name": "urllib3", + "version": "2.6.3", + "purl": "pkg:pypi/urllib3@2.6.3", + "type": "library", + "bom-ref": "pkg:pypi/urllib3@2.6.3" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/sub-pkg@0.1.0", + "dependsOn": [ + "pkg:pypi/requests@2.33.1" + ] + }, + { + "ref": "pkg:pypi/requests@2.33.1", + "dependsOn": [ + "pkg:pypi/certifi@2026.2.25", + "pkg:pypi/charset-normalizer@3.4.7", + "pkg:pypi/idna@3.11", + "pkg:pypi/urllib3@2.6.3" + ] + }, + { + "ref": "pkg:pypi/certifi@2026.2.25", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/charset-normalizer@3.4.7", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/idna@3.11", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/urllib3@2.6.3", + "dependsOn": [] + } + ] }