diff --git a/src/provider.js b/src/provider.js index 0a1fa94..8c220e3 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 pythonPyprojectProvider from './providers/python_pyproject.js' import rustCargoProvider from './providers/rust_cargo.js' /** @typedef {{ecosystem: string, contentType: string, content: string}} Provided */ @@ -26,6 +27,7 @@ export const availableProviders = [ new Javascript_npm(), golangGomodulesProvider, pythonPipProvider, + pythonPyprojectProvider, rustCargoProvider] /** diff --git a/src/providers/python_pip.js b/src/providers/python_pip.js index 139b454..64bcfb0 100644 --- a/src/providers/python_pip.js +++ b/src/providers/python_pip.js @@ -87,7 +87,7 @@ function addAllDependencies(source, dep, sbom) { sbom.addDependency(source, targetPurl) let directDeps = dep["dependencies"] if (directDeps !== undefined && directDeps.length > 0) { - directDeps.forEach((dependency) => { addAllDependencies(toPurl(dep["name"],dep["version"]), dependency, sbom) }) + directDeps.forEach((dependency) => { addAllDependencies(toPurl(dep["name"], dep["version"]), dependency, sbom) }) } } @@ -123,7 +123,7 @@ async function getIgnoredDependencies(manifest) { async function handleIgnoredDependencies(manifest, sbom, opts = {}) { let ignoredDeps = await getIgnoredDependencies(manifest) let matchManifestVersions = getCustom("MATCH_MANIFEST_VERSIONS", "true", opts); - if(matchManifestVersions === "true") { + if (matchManifestVersions === "true") { const ignoredDepsVersion = ignoredDeps.filter(dep => dep.version !== undefined); sbom.filterIgnoredDepsIncludingVersion(ignoredDepsVersion.map(dep => dep.toString())) } else { @@ -150,7 +150,7 @@ function getPythonPipBinaries(binaries, opts) { invokeCommand(python, ['--version']) invokeCommand(pip, ['--version']) } catch (error) { - throw new Error(`Failed checking for python/pip binaries from supplied environment variables`, {cause: error}) + throw new Error(`Failed checking for python/pip binaries from supplied environment variables`, { cause: error }) } } binaries.pip = pip @@ -240,6 +240,6 @@ async function getSbomForComponentAnalysis(manifest, opts = {}) { * @param version * @return {PackageURL} */ -function toPurl(name,version) { +function toPurl(name, version) { return new PackageURL('pypi', undefined, name, version, undefined, undefined); } diff --git a/src/providers/python_pyproject.js b/src/providers/python_pyproject.js new file mode 100644 index 0000000..4526dbb --- /dev/null +++ b/src/providers/python_pyproject.js @@ -0,0 +1,351 @@ +import fs from 'node:fs' +import path from 'node:path' + +import { PackageURL } from 'packageurl-js' +import { parse as parseToml } from 'smol-toml' + +import { getLicense } from '../license/license_utils.js' +import Sbom from '../sbom.js' + +export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest } + +const ecosystem = 'pip' + +const IGNORE_MARKERS = ['exhortignore', 'trustify-da-ignore'] + +const DEFAULT_ROOT_NAME = 'default-pip-root' +const DEFAULT_ROOT_VERSION = '0.0.0' + +/** + * @param {string} manifestName + * @returns {boolean} + */ +function isSupported(manifestName) { + return 'pyproject.toml' === manifestName +} + +function validateLockFile() { return true } + +/** + * Read project license from pyproject.toml, with fallback to LICENSE file. + * @param {string} manifestPath + * @returns {string|null} + */ +function readLicenseFromManifest(manifestPath) { + let fromManifest = null + try { + let content = fs.readFileSync(manifestPath, 'utf-8') + let parsed = parseToml(content) + fromManifest = parsed.project?.license + if (typeof fromManifest === 'object' && fromManifest != null) { + fromManifest = fromManifest.text || null + } + if (!fromManifest) { + fromManifest = parsed.tool?.poetry?.license || null + } + } catch (_) { + // leave fromManifest as null + } + return getLicense(fromManifest, manifestPath) +} + +/** + * Canonicalize a Python package name per PEP 503. + * @param {string} name + * @returns {string} + */ +function canonicalize(name) { + return name.toLowerCase().replace(/[-_.]+/g, '-') +} + +/** + * Parse direct runtime dependency names from pyproject.toml. + * Versions come from the lock file, not the manifest. + * @param {object} parsed - parsed TOML object + * @returns {string[]} + */ +function parseDirectDeps(parsed) { + // PEP 621: project.dependencies + if (parsed.project?.dependencies && Array.isArray(parsed.project.dependencies)) { + return parsed.project.dependencies.map(req => { + let match = req.match(/^([A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)/) + return match ? match[1] : req.trim() + }) + } + // Poetry: tool.poetry.dependencies + if (parsed.tool?.poetry?.dependencies) { + return Object.keys(parsed.tool.poetry.dependencies) + .filter(name => name.toLowerCase() !== 'python') + } + return [] +} + +/** + * Detect which lock file to use based on manifest content and available files. + * @param {string} manifestDir + * @param {object} parsed - parsed pyproject.toml + * @returns {{type: string, path: string}|null} + */ +function findLockFile(manifestDir, parsed) { + let hasPoetry = !!(parsed.tool?.poetry) + let hasPdm = !!(parsed.tool?.pdm) + + let poetryLock = path.join(manifestDir, 'poetry.lock') + let pdmLock = path.join(manifestDir, 'pdm.lock') + let uvLock = path.join(manifestDir, 'uv.lock') + + // prefer lock file matching the declared tool + if (hasPoetry && fs.existsSync(poetryLock)) { return { type: 'poetry', path: poetryLock } } + if (hasPdm && fs.existsSync(pdmLock)) { return { type: 'pdm', path: pdmLock } } + + // then check uv (no pyproject.toml marker needed) + if (fs.existsSync(uvLock)) { return { type: 'uv', path: uvLock } } + + // fallback: any available lock file + if (fs.existsSync(poetryLock)) { return { type: 'poetry', path: poetryLock } } + if (fs.existsSync(pdmLock)) { return { type: 'pdm', path: pdmLock } } + + return null +} + +/** + * Parse a lock file into a normalized dependency graph. + * @param {{type: string, path: string}} lockInfo + * @returns {Map} + */ +function parseLockFile(lockInfo) { + let content = fs.readFileSync(lockInfo.path, 'utf-8') + let parsed = parseToml(content) + let graph = new Map() + + let packages = parsed.package || [] + for (let pkg of packages) { + let name = pkg.name + let version = pkg.version + if (!name || !version) { continue } + + let deps = [] + if (lockInfo.type === 'poetry') { + deps = parsePoetryDeps(pkg) + } else if (lockInfo.type === 'pdm') { + deps = parsePdmDeps(pkg) + } else if (lockInfo.type === 'uv') { + deps = parseUvDeps(pkg) + } + + let key = canonicalize(name) + graph.set(key, { name, version, dependencies: deps.map(d => canonicalize(d)) }) + } + + return graph +} + +/** + * Extract dependency names from a poetry.lock [[package]] entry. + * @param {object} pkg + * @returns {string[]} + */ +function parsePoetryDeps(pkg) { + let deps = pkg.dependencies + if (!deps || typeof deps !== 'object') { return [] } + return Object.keys(deps) +} + +/** + * Extract dependency names from a pdm.lock [[package]] entry. + * Dependencies are strings like "dep-name>=1.0". + * @param {object} pkg + * @returns {string[]} + */ +function parsePdmDeps(pkg) { + let deps = pkg.dependencies || [] + if (!Array.isArray(deps)) { return [] } + return deps.map(d => { + if (typeof d === 'string') { + let match = d.match(/^([A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)/) + return match ? match[1] : d.trim() + } + return '' + }).filter(Boolean) +} + +/** + * Extract dependency names from a uv.lock [[package]] entry. + * Dependencies are objects like {name: "dep"}. + * @param {object} pkg + * @returns {string[]} + */ +function parseUvDeps(pkg) { + let deps = pkg.dependencies || [] + if (!Array.isArray(deps)) { return [] } + return deps.map(d => { + if (typeof d === 'object' && d != null && d.name) { return d.name } + if (typeof d === 'string') { return d } + return '' + }).filter(Boolean) +} + +/** + * Scan raw pyproject.toml text for dependencies with ignore markers. + * Returns a Set of canonicalized dependency names. + * @param {string} manifestPath + * @returns {Set} + */ +function getIgnoredDeps(manifestPath) { + let ignored = new Set() + let content = fs.readFileSync(manifestPath, 'utf-8') + let lines = content.split(/\r?\n/) + + for (let line of lines) { + if (!IGNORE_MARKERS.some(m => line.includes(m))) { continue } + + // PEP 621 style: "requests>=2.25" #exhortignore + let pep621Match = line.match(/^\s*"([^"]+)"/) + if (pep621Match) { + let reqStr = pep621Match[1] + let nameMatch = reqStr.match(/^([A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)/) + if (nameMatch) { + ignored.add(canonicalize(nameMatch[1])) + } + continue + } + + // Poetry style: requests = "^2.25" #exhortignore + let poetryMatch = line.match(/^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*=/) + if (poetryMatch) { + ignored.add(canonicalize(poetryMatch[1])) + } + } + + return ignored +} + +/** + * Build a dependency tree from the lock file graph, starting from direct deps. + * @param {Map} graph - normalized dependency graph from lock file + * @param {string[]} directDeps + * @param {Set} ignoredDeps + * @param {boolean} includeTransitive + * @returns {{name: string, version: string, dependencies: object[]}[]} + */ +function buildDependencyTree(graph, directDeps, ignoredDeps, includeTransitive) { + let result = [] + + for (let name of directDeps) { + let key = canonicalize(name) + if (ignoredDeps.has(key)) { continue } + + let entry = graph.get(key) + if (!entry) { continue } + + let depTree = [] + if (includeTransitive) { + let visited = new Set() + visited.add(key) + collectTransitive(graph, entry.dependencies, depTree, ignoredDeps, visited) + } + + 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. + */ +function collectTransitive(graph, depKeys, result, ignoredDeps, visited) { + for (let depKey of depKeys) { + let canonKey = canonicalize(depKey) + if (ignoredDeps.has(canonKey)) { continue } + if (visited.has(canonKey)) { continue } + visited.add(canonKey) + + let entry = graph.get(canonKey) + if (!entry) { continue } + + let childDeps = [] + collectTransitive(graph, entry.dependencies, childDeps, ignoredDeps, visited) + + result.push({ name: entry.name, version: entry.version, dependencies: childDeps }) + } + + result.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())) +} + +function toPurl(name, version) { + return new PackageURL('pypi', undefined, name, version, undefined, undefined) +} + +function addAllDependencies(source, dep, sbom) { + let targetPurl = toPurl(dep.name, dep.version) + sbom.addDependency(source, targetPurl) + if (dep.dependencies && dep.dependencies.length > 0) { + dep.dependencies.forEach(child => addAllDependencies(toPurl(dep.name, dep.version), child, sbom)) + } +} + +/** + * @param {string} manifest - path to pyproject.toml + * @param {{}} [opts={}] + * @returns {Promise} + */ +async function provideStack(manifest, opts = {}) { + return { + ecosystem, + content: createSbom(manifest, opts, true), + contentType: 'application/vnd.cyclonedx+json' + } +} + +/** + * @param {string} manifest - path to pyproject.toml + * @param {{}} [opts={}] + * @returns {Promise} + */ +async function provideComponent(manifest, opts = {}) { + return { + ecosystem, + content: createSbom(manifest, opts, false), + contentType: 'application/vnd.cyclonedx+json' + } +} + +/** + * Create SBOM json string for a pyproject.toml project. + * @param {string} manifest - path to pyproject.toml + * @param {{}} opts + * @param {boolean} includeTransitive + * @returns {string} + */ +function createSbom(manifest, opts, includeTransitive) { + let manifestDir = path.dirname(manifest) + let content = fs.readFileSync(manifest, 'utf-8') + let parsed = parseToml(content) + + let lockInfo = findLockFile(manifestDir, parsed) + if (!lockInfo) { + throw new Error('pyproject.toml requires a lock file (poetry.lock, pdm.lock, or uv.lock) in the same directory') + } + + let directDeps = parseDirectDeps(parsed) + let graph = parseLockFile(lockInfo) + let ignoredDeps = getIgnoredDeps(manifest) + let dependencies = buildDependencyTree(graph, directDeps, ignoredDeps, includeTransitive) + + let sbom = new Sbom() + let rootPurl = toPurl(DEFAULT_ROOT_NAME, DEFAULT_ROOT_VERSION) + let license = readLicenseFromManifest(manifest) + sbom.addRoot(rootPurl, license) + + dependencies.forEach(dep => { + if (includeTransitive) { + addAllDependencies(rootPurl, dep, sbom) + } else { + sbom.addDependency(rootPurl, toPurl(dep.name, dep.version)) + } + }) + + return sbom.getAsJsonString(opts) +} diff --git a/test/provider.test.js b/test/provider.test.js index 87e383b..034eaa6 100644 --- a/test/provider.test.js +++ b/test/provider.test.js @@ -124,6 +124,12 @@ suite('testing the matchForLicense utility function', () => { expect(provider.isSupported('requirements.txt')).to.be.true; }); + test('should match Python pyproject provider for pyproject.toml', () => { + const provider = matchForLicense('/some/path/pyproject.toml', availableProviders); + expect(provider).to.exist; + expect(provider.isSupported('pyproject.toml')).to.be.true; + }); + test('should match Rust Cargo provider for Cargo.toml', () => { const provider = matchForLicense('/some/path/Cargo.toml', availableProviders); expect(provider).to.exist; @@ -138,6 +144,7 @@ suite('testing the matchForLicense utility function', () => { 'package.json', 'go.mod', 'requirements.txt', + 'pyproject.toml', 'Cargo.toml' ]; diff --git a/test/providers/python_pyproject.test.js b/test/providers/python_pyproject.test.js new file mode 100644 index 0000000..d498e39 --- /dev/null +++ b/test/providers/python_pyproject.test.js @@ -0,0 +1,170 @@ +import fs from 'fs' + +import { expect } from 'chai' +import { useFakeTimers } from 'sinon' + +import pythonPyproject from '../../src/providers/python_pyproject.js' + +let clock + +suite('testing the python-pyproject data provider', () => { + [ + {name: 'pyproject.toml', expected: true}, + {name: 'requirements.txt', expected: false}, + {name: 'Cargo.toml', expected: false}, + ].forEach(testCase => { + test(`verify isSupported returns ${testCase.expected} for ${testCase.name}`, () => + expect(pythonPyproject.isSupported(testCase.name)).to.equal(testCase.expected) + ) + }); + + test('verify validateLockFile always returns true', () => { + expect(pythonPyproject.validateLockFile()).to.equal(true) + }); + + ['poetry_lock', 'pdm_lock', 'uv_lock', 'poetry_only_deps', 'pep621_ignore_and_extras'].forEach(testCase => { + let scenario = testCase.replaceAll('_', ' ') + + test(`verify pyproject.toml sbom provided for stack analysis with ${scenario}`, async () => { + let expectedSbom = fs.readFileSync(`test/providers/tst_manifests/pyproject/${testCase}/expected_stack_sbom.json`).toString() + expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) + + let providedData = await pythonPyproject.provideStack(`test/providers/tst_manifests/pyproject/${testCase}/pyproject.toml`) + expect(providedData).to.deep.equal({ + ecosystem: 'pip', + contentType: 'application/vnd.cyclonedx+json', + content: expectedSbom + }) + }) + + test(`verify pyproject.toml sbom provided for component analysis with ${scenario}`, async () => { + let expectedSbom = fs.readFileSync(`test/providers/tst_manifests/pyproject/${testCase}/expected_component_sbom.json`).toString().trim() + expectedSbom = JSON.stringify(JSON.parse(expectedSbom)) + + let providedData = await pythonPyproject.provideComponent(`test/providers/tst_manifests/pyproject/${testCase}/pyproject.toml`) + expect(providedData).to.deep.equal({ + ecosystem: 'pip', + contentType: 'application/vnd.cyclonedx+json', + content: expectedSbom + }) + }) + }) + + suite('exhortignore filtering', () => { + test('exhortignore excludes dep from component analysis', async () => { + let providedData = await pythonPyproject.provideComponent('test/providers/tst_manifests/pyproject/poetry_lock/pyproject.toml') + let sbom = JSON.parse(providedData.content) + let componentNames = sbom.components.map(c => c.name) + expect(componentNames).to.not.include('click') + expect(componentNames).to.include('flask') + expect(componentNames).to.include('requests') + }) + + test('trustify-da-ignore excludes dep with extras from component analysis', async () => { + let providedData = await pythonPyproject.provideComponent('test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml') + let sbom = JSON.parse(providedData.content) + let componentNames = sbom.components.map(c => c.name) + expect(componentNames).to.not.include('uvicorn') + expect(componentNames).to.not.include('markupsafe') + expect(componentNames).to.include('flask') + expect(componentNames).to.include('requests') + expect(componentNames).to.include('typing-extensions') + expect(componentNames).to.include('pywin32') + }) + + test('ignored transitive dep is excluded from stack analysis tree', async () => { + let providedData = await pythonPyproject.provideStack('test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml') + let sbom = JSON.parse(providedData.content) + let componentNames = sbom.components.map(c => c.name) + // markupsafe is exhortignored, so even though jinja2 depends on it, it should be absent + expect(componentNames).to.not.include('markupsafe') + // jinja2 should still be present (as transitive dep of flask) but with no children + let jinja2Dep = sbom.dependencies.find(d => d.ref.includes('jinja2')) + expect(jinja2Dep).to.exist + expect(jinja2Dep.dependsOn).to.deep.equal([]) + }) + }) + + suite('name canonicalization', () => { + test('underscore in manifest matches hyphen in lock file', async () => { + let providedData = await pythonPyproject.provideComponent('test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml') + let sbom = JSON.parse(providedData.content) + let typingExt = sbom.components.find(c => c.name === 'typing-extensions') + expect(typingExt).to.exist + expect(typingExt.version).to.equal('4.1.1') + }) + + test('mixed case in manifest matches lowercase in lock file', async () => { + let providedData = await pythonPyproject.provideComponent('test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml') + let sbom = JSON.parse(providedData.content) + let flask = sbom.components.find(c => c.name === 'flask') + expect(flask).to.exist + expect(flask.version).to.equal('2.0.3') + }) + }) + + suite('poetry-only project', () => { + test('reads deps from tool.poetry.dependencies when no project section exists', async () => { + let providedData = await pythonPyproject.provideComponent('test/providers/tst_manifests/pyproject/poetry_only_deps/pyproject.toml') + let sbom = JSON.parse(providedData.content) + let componentNames = sbom.components.map(c => c.name) + expect(componentNames).to.include('flask') + expect(componentNames).to.include('requests') + expect(componentNames).to.include('click') + }) + + test('filters out python from poetry dependencies', async () => { + let providedData = await pythonPyproject.provideComponent('test/providers/tst_manifests/pyproject/poetry_only_deps/pyproject.toml') + let sbom = JSON.parse(providedData.content) + let componentNames = sbom.components.map(c => c.name) + expect(componentNames).to.not.include('python') + }) + + test('dep that is both direct and transitive appears once in components', async () => { + let providedData = await pythonPyproject.provideStack('test/providers/tst_manifests/pyproject/poetry_only_deps/pyproject.toml') + let sbom = JSON.parse(providedData.content) + // click is a direct dep and also a transitive dep of flask + let clickComponents = sbom.components.filter(c => c.name === 'click') + expect(clickComponents).to.have.lengthOf(1) + }) + }) + + suite('PEP 508 features', () => { + test('extras in dep spec do not break name extraction', async () => { + // uvicorn[standard] is ignored, but we still need to verify the name was parsed correctly + // by checking that it IS in the ignored set (absent from output) + let providedData = await pythonPyproject.provideComponent('test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml') + let sbom = JSON.parse(providedData.content) + let componentNames = sbom.components.map(c => c.name) + expect(componentNames).to.not.include('uvicorn') + expect(componentNames).to.not.include('uvicorn[standard]') + }) + + test('environment markers do not break name extraction', async () => { + let providedData = await pythonPyproject.provideComponent('test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml') + let sbom = JSON.parse(providedData.content) + let pywin32 = sbom.components.find(c => c.name === 'pywin32') + expect(pywin32).to.exist + expect(pywin32.version).to.equal('300') + }) + }) + + test('throws when no lock file is present', async () => { + try { + await pythonPyproject.provideStack('test/providers/tst_manifests/pyproject/no_lock_file_dummy/pyproject.toml') + expect.fail('should have thrown') + } catch (e) { + expect(e.message).to.include('lock file') + } + }) + +}).beforeAll(() => { + clock = useFakeTimers(new Date('2023-10-01T00:00:00.000Z')) + // create a minimal pyproject.toml with no lock file for the error test + fs.mkdirSync('test/providers/tst_manifests/pyproject/no_lock_file_dummy', { recursive: true }) + fs.writeFileSync('test/providers/tst_manifests/pyproject/no_lock_file_dummy/pyproject.toml', + '[project]\nname = "test"\nversion = "1.0.0"\ndependencies = ["requests>=2.0"]') +}).afterAll(() => { + clock.restore() + fs.rmSync('test/providers/tst_manifests/pyproject/no_lock_file_dummy', { recursive: true, force: true }) +}) diff --git a/test/providers/tst_manifests/pyproject/pdm_lock/expected_component_sbom.json b/test/providers/tst_manifests/pyproject/pdm_lock/expected_component_sbom.json new file mode 100644 index 0000000..88c5641 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/pdm_lock/expected_component_sbom.json @@ -0,0 +1,48 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "default-pip-root", + "version": "0.0.0", + "purl": "pkg:pypi/default-pip-root@0.0.0", + "type": "application", + "bom-ref": "pkg:pypi/default-pip-root@0.0.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": "requests", + "version": "2.25.1", + "purl": "pkg:pypi/requests@2.25.1", + "type": "library", + "bom-ref": "pkg:pypi/requests@2.25.1" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/default-pip-root@0.0.0", + "dependsOn": [ + "pkg:pypi/flask@2.0.3", + "pkg:pypi/requests@2.25.1" + ] + }, + { + "ref": "pkg:pypi/flask@2.0.3", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/requests@2.25.1", + "dependsOn": [] + } + ] +} diff --git a/test/providers/tst_manifests/pyproject/pdm_lock/expected_stack_sbom.json b/test/providers/tst_manifests/pyproject/pdm_lock/expected_stack_sbom.json new file mode 100644 index 0000000..6cbe35a --- /dev/null +++ b/test/providers/tst_manifests/pyproject/pdm_lock/expected_stack_sbom.json @@ -0,0 +1,147 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "default-pip-root", + "version": "0.0.0", + "purl": "pkg:pypi/default-pip-root@0.0.0", + "type": "application", + "bom-ref": "pkg:pypi/default-pip-root@0.0.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": "itsdangerous", + "version": "2.0.1", + "purl": "pkg:pypi/itsdangerous@2.0.1", + "type": "library", + "bom-ref": "pkg:pypi/itsdangerous@2.0.1" + }, + { + "name": "jinja2", + "version": "3.0.3", + "purl": "pkg:pypi/jinja2@3.0.3", + "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", + "purl": "pkg:pypi/werkzeug@2.0.3", + "type": "library", + "bom-ref": "pkg:pypi/werkzeug@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": "certifi", + "version": "2023.7.22", + "purl": "pkg:pypi/certifi@2023.7.22", + "type": "library", + "bom-ref": "pkg:pypi/certifi@2023.7.22" + }, + { + "name": "chardet", + "version": "4.0.0", + "purl": "pkg:pypi/chardet@4.0.0", + "type": "library", + "bom-ref": "pkg:pypi/chardet@4.0.0" + }, + { + "name": "idna", + "version": "2.10", + "purl": "pkg:pypi/idna@2.10", + "type": "library", + "bom-ref": "pkg:pypi/idna@2.10" + }, + { + "name": "urllib3", + "version": "1.26.16", + "purl": "pkg:pypi/urllib3@1.26.16", + "type": "library", + "bom-ref": "pkg:pypi/urllib3@1.26.16" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/default-pip-root@0.0.0", + "dependsOn": [ + "pkg:pypi/flask@2.0.3", + "pkg:pypi/requests@2.25.1" + ] + }, + { + "ref": "pkg:pypi/flask@2.0.3", + "dependsOn": [ + "pkg:pypi/itsdangerous@2.0.1", + "pkg:pypi/jinja2@3.0.3", + "pkg:pypi/werkzeug@2.0.3" + ] + }, + { + "ref": "pkg:pypi/itsdangerous@2.0.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/jinja2@3.0.3", + "dependsOn": [ + "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/certifi@2023.7.22", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/chardet@4.0.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/idna@2.10", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/urllib3@1.26.16", + "dependsOn": [] + } + ] +} diff --git a/test/providers/tst_manifests/pyproject/pdm_lock/pdm.lock b/test/providers/tst_manifests/pyproject/pdm_lock/pdm.lock new file mode 100644 index 0000000..b998f1a --- /dev/null +++ b/test/providers/tst_manifests/pyproject/pdm_lock/pdm.lock @@ -0,0 +1,59 @@ +[metadata] +groups = ["default"] +strategy = ["cross_platform"] +lock_version = "4.4" +content_hash = "sha256:abc123" + +[[package]] +name = "certifi" +version = "2023.7.22" + +[[package]] +name = "chardet" +version = "4.0.0" + +[[package]] +name = "flask" +version = "2.0.3" +dependencies = [ + "itsdangerous>=2.0", + "Jinja2>=3.0", + "Werkzeug>=2.0", +] + +[[package]] +name = "idna" +version = "2.10" + +[[package]] +name = "itsdangerous" +version = "2.0.1" + +[[package]] +name = "jinja2" +version = "3.0.3" +dependencies = [ + "MarkupSafe>=2.0", +] + +[[package]] +name = "markupsafe" +version = "2.0.1" + +[[package]] +name = "requests" +version = "2.25.1" +dependencies = [ + "certifi>=2017.4.17", + "chardet>=3.0.2,<5", + "idna>=2.5,<3", + "urllib3>=1.21.1,<1.27", +] + +[[package]] +name = "urllib3" +version = "1.26.16" + +[[package]] +name = "werkzeug" +version = "2.0.3" diff --git a/test/providers/tst_manifests/pyproject/pdm_lock/pyproject.toml b/test/providers/tst_manifests/pyproject/pdm_lock/pyproject.toml new file mode 100644 index 0000000..87beb7a --- /dev/null +++ b/test/providers/tst_manifests/pyproject/pdm_lock/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "test-project" +version = "1.0.0" +dependencies = [ + "flask==2.0.3", + "requests==2.25.1", +] + +[tool.pdm] 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 new file mode 100644 index 0000000..14b5671 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_component_sbom.json @@ -0,0 +1,72 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "default-pip-root", + "version": "0.0.0", + "purl": "pkg:pypi/default-pip-root@0.0.0", + "type": "application", + "bom-ref": "pkg:pypi/default-pip-root@0.0.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": "pywin32", + "version": "300", + "purl": "pkg:pypi/pywin32@300", + "type": "library", + "bom-ref": "pkg:pypi/pywin32@300" + }, + { + "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" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/default-pip-root@0.0.0", + "dependsOn": [ + "pkg:pypi/flask@2.0.3", + "pkg:pypi/pywin32@300", + "pkg:pypi/requests@2.25.1", + "pkg:pypi/typing-extensions@4.1.1" + ] + }, + { + "ref": "pkg:pypi/flask@2.0.3", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pywin32@300", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/requests@2.25.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/typing-extensions@4.1.1", + "dependsOn": [] + } + ] +} 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 new file mode 100644 index 0000000..f0082d5 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/expected_stack_sbom.json @@ -0,0 +1,158 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "default-pip-root", + "version": "0.0.0", + "purl": "pkg:pypi/default-pip-root@0.0.0", + "type": "application", + "bom-ref": "pkg:pypi/default-pip-root@0.0.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": "itsdangerous", + "version": "2.0.1", + "purl": "pkg:pypi/itsdangerous@2.0.1", + "type": "library", + "bom-ref": "pkg:pypi/itsdangerous@2.0.1" + }, + { + "name": "jinja2", + "version": "3.0.3", + "purl": "pkg:pypi/jinja2@3.0.3", + "type": "library", + "bom-ref": "pkg:pypi/jinja2@3.0.3" + }, + { + "name": "werkzeug", + "version": "2.0.3", + "purl": "pkg:pypi/werkzeug@2.0.3", + "type": "library", + "bom-ref": "pkg:pypi/werkzeug@2.0.3" + }, + { + "name": "pywin32", + "version": "300", + "purl": "pkg:pypi/pywin32@300", + "type": "library", + "bom-ref": "pkg:pypi/pywin32@300" + }, + { + "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", + "purl": "pkg:pypi/certifi@2023.7.22", + "type": "library", + "bom-ref": "pkg:pypi/certifi@2023.7.22" + }, + { + "name": "chardet", + "version": "4.0.0", + "purl": "pkg:pypi/chardet@4.0.0", + "type": "library", + "bom-ref": "pkg:pypi/chardet@4.0.0" + }, + { + "name": "idna", + "version": "2.10", + "purl": "pkg:pypi/idna@2.10", + "type": "library", + "bom-ref": "pkg:pypi/idna@2.10" + }, + { + "name": "urllib3", + "version": "1.26.16", + "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": [ + { + "ref": "pkg:pypi/default-pip-root@0.0.0", + "dependsOn": [ + "pkg:pypi/flask@2.0.3", + "pkg:pypi/pywin32@300", + "pkg:pypi/requests@2.25.1", + "pkg:pypi/typing-extensions@4.1.1" + ] + }, + { + "ref": "pkg:pypi/flask@2.0.3", + "dependsOn": [ + "pkg:pypi/itsdangerous@2.0.1", + "pkg:pypi/jinja2@3.0.3", + "pkg:pypi/werkzeug@2.0.3" + ] + }, + { + "ref": "pkg:pypi/itsdangerous@2.0.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/jinja2@3.0.3", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/werkzeug@2.0.3", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pywin32@300", + "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": [] + }, + { + "ref": "pkg:pypi/chardet@4.0.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/idna@2.10", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/urllib3@1.26.16", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/typing-extensions@4.1.1", + "dependsOn": [] + } + ] +} diff --git a/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml b/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml new file mode 100644 index 0000000..d9e691d --- /dev/null +++ b/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "test-project" +version = "1.0.0" +dependencies = [ + "Flask==2.0.3", + "typing_extensions==4.1.1", + "uvicorn[standard]==0.17.0", # trustify-da-ignore + "MarkupSafe==2.0.1", #exhortignore + "requests==2.25.1", + "pywin32==300; sys_platform == 'win32'", +] diff --git a/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/uv.lock b/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/uv.lock new file mode 100644 index 0000000..9044ca0 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/pep621_ignore_and_extras/uv.lock @@ -0,0 +1,92 @@ +version = 1 +requires-python = ">=3.8" + +[[package]] +name = "certifi" +version = "2023.7.22" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "chardet" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "flask" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug" }, +] + +[[package]] +name = "idna" +version = "2.10" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "itsdangerous" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "jinja2" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] + +[[package]] +name = "markupsafe" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "pywin32" +version = "300" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "requests" +version = "2.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "chardet" }, + { name = "idna" }, + { name = "urllib3" }, +] + +[[package]] +name = "test-project" +version = "1.0.0" +source = { virtual = "." } +dependencies = [ + { name = "flask" }, + { name = "typing-extensions" }, + { name = "requests" }, + { name = "pywin32" }, +] + +[[package]] +name = "typing-extensions" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "urllib3" +version = "1.26.16" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "uvicorn" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "werkzeug" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } 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 new file mode 100644 index 0000000..88c5641 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/poetry_lock/expected_component_sbom.json @@ -0,0 +1,48 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "default-pip-root", + "version": "0.0.0", + "purl": "pkg:pypi/default-pip-root@0.0.0", + "type": "application", + "bom-ref": "pkg:pypi/default-pip-root@0.0.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": "requests", + "version": "2.25.1", + "purl": "pkg:pypi/requests@2.25.1", + "type": "library", + "bom-ref": "pkg:pypi/requests@2.25.1" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/default-pip-root@0.0.0", + "dependsOn": [ + "pkg:pypi/flask@2.0.3", + "pkg:pypi/requests@2.25.1" + ] + }, + { + "ref": "pkg:pypi/flask@2.0.3", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/requests@2.25.1", + "dependsOn": [] + } + ] +} 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 new file mode 100644 index 0000000..6cbe35a --- /dev/null +++ b/test/providers/tst_manifests/pyproject/poetry_lock/expected_stack_sbom.json @@ -0,0 +1,147 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "default-pip-root", + "version": "0.0.0", + "purl": "pkg:pypi/default-pip-root@0.0.0", + "type": "application", + "bom-ref": "pkg:pypi/default-pip-root@0.0.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": "itsdangerous", + "version": "2.0.1", + "purl": "pkg:pypi/itsdangerous@2.0.1", + "type": "library", + "bom-ref": "pkg:pypi/itsdangerous@2.0.1" + }, + { + "name": "jinja2", + "version": "3.0.3", + "purl": "pkg:pypi/jinja2@3.0.3", + "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", + "purl": "pkg:pypi/werkzeug@2.0.3", + "type": "library", + "bom-ref": "pkg:pypi/werkzeug@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": "certifi", + "version": "2023.7.22", + "purl": "pkg:pypi/certifi@2023.7.22", + "type": "library", + "bom-ref": "pkg:pypi/certifi@2023.7.22" + }, + { + "name": "chardet", + "version": "4.0.0", + "purl": "pkg:pypi/chardet@4.0.0", + "type": "library", + "bom-ref": "pkg:pypi/chardet@4.0.0" + }, + { + "name": "idna", + "version": "2.10", + "purl": "pkg:pypi/idna@2.10", + "type": "library", + "bom-ref": "pkg:pypi/idna@2.10" + }, + { + "name": "urllib3", + "version": "1.26.16", + "purl": "pkg:pypi/urllib3@1.26.16", + "type": "library", + "bom-ref": "pkg:pypi/urllib3@1.26.16" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/default-pip-root@0.0.0", + "dependsOn": [ + "pkg:pypi/flask@2.0.3", + "pkg:pypi/requests@2.25.1" + ] + }, + { + "ref": "pkg:pypi/flask@2.0.3", + "dependsOn": [ + "pkg:pypi/itsdangerous@2.0.1", + "pkg:pypi/jinja2@3.0.3", + "pkg:pypi/werkzeug@2.0.3" + ] + }, + { + "ref": "pkg:pypi/itsdangerous@2.0.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/jinja2@3.0.3", + "dependsOn": [ + "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/certifi@2023.7.22", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/chardet@4.0.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/idna@2.10", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/urllib3@1.26.16", + "dependsOn": [] + } + ] +} diff --git a/test/providers/tst_manifests/pyproject/poetry_lock/poetry.lock b/test/providers/tst_manifests/pyproject/poetry_lock/poetry.lock new file mode 100644 index 0000000..ffdf4e6 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/poetry_lock/poetry.lock @@ -0,0 +1,68 @@ +[[package]] +name = "flask" +version = "2.0.3" +description = "A simple framework for building complex web applications." + +[package.dependencies] +itsdangerous = ">=2.0" +Jinja2 = ">=3.0" +Werkzeug = ">=2.0" + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[[package]] +name = "click" +version = "8.0.4" +description = "Composable command line interface toolkit" + +[[package]] +name = "itsdangerous" +version = "2.0.1" +description = "Safely pass data to untrusted environments and back." + +[[package]] +name = "jinja2" +version = "3.0.3" +description = "A small but fast and easy to use stand-alone template engine written in pure python." + +[package.dependencies] +MarkupSafe = ">=2.0" + +[[package]] +name = "werkzeug" +version = "2.0.3" +description = "The comprehensive WSGI web application library." + +[[package]] +name = "markupsafe" +version = "2.0.1" +description = "Safely add untrusted strings to HTML/XML markup." + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 3" + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" + +[[package]] +name = "urllib3" +version = "1.26.16" +description = "HTTP library with thread-safe connection pooling, file post, and more." diff --git a/test/providers/tst_manifests/pyproject/poetry_lock/pyproject.toml b/test/providers/tst_manifests/pyproject/poetry_lock/pyproject.toml new file mode 100644 index 0000000..0dc2be8 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/poetry_lock/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "test-project" +version = "1.0.0" +dependencies = [ + "flask==2.0.3", + "requests==2.25.1", + "click==8.0.4", #exhortignore +] + +[tool.poetry] +name = "test-project" diff --git a/test/providers/tst_manifests/pyproject/poetry_only_deps/expected_component_sbom.json b/test/providers/tst_manifests/pyproject/poetry_only_deps/expected_component_sbom.json new file mode 100644 index 0000000..12b68b6 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/poetry_only_deps/expected_component_sbom.json @@ -0,0 +1,60 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "default-pip-root", + "version": "0.0.0", + "purl": "pkg:pypi/default-pip-root@0.0.0", + "type": "application", + "bom-ref": "pkg:pypi/default-pip-root@0.0.0" + } + }, + "components": [ + { + "name": "click", + "version": "8.0.4", + "purl": "pkg:pypi/click@8.0.4", + "type": "library", + "bom-ref": "pkg:pypi/click@8.0.4" + }, + { + "name": "flask", + "version": "2.0.3", + "purl": "pkg:pypi/flask@2.0.3", + "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" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/default-pip-root@0.0.0", + "dependsOn": [ + "pkg:pypi/click@8.0.4", + "pkg:pypi/flask@2.0.3", + "pkg:pypi/requests@2.25.1" + ] + }, + { + "ref": "pkg:pypi/click@8.0.4", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/flask@2.0.3", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/requests@2.25.1", + "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 new file mode 100644 index 0000000..55d5400 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/poetry_only_deps/expected_stack_sbom.json @@ -0,0 +1,160 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "default-pip-root", + "version": "0.0.0", + "purl": "pkg:pypi/default-pip-root@0.0.0", + "type": "application", + "bom-ref": "pkg:pypi/default-pip-root@0.0.0" + } + }, + "components": [ + { + "name": "click", + "version": "8.0.4", + "purl": "pkg:pypi/click@8.0.4", + "type": "library", + "bom-ref": "pkg:pypi/click@8.0.4" + }, + { + "name": "flask", + "version": "2.0.3", + "purl": "pkg:pypi/flask@2.0.3", + "type": "library", + "bom-ref": "pkg:pypi/flask@2.0.3" + }, + { + "name": "itsdangerous", + "version": "2.0.1", + "purl": "pkg:pypi/itsdangerous@2.0.1", + "type": "library", + "bom-ref": "pkg:pypi/itsdangerous@2.0.1" + }, + { + "name": "jinja2", + "version": "3.0.3", + "purl": "pkg:pypi/jinja2@3.0.3", + "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", + "purl": "pkg:pypi/werkzeug@2.0.3", + "type": "library", + "bom-ref": "pkg:pypi/werkzeug@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": "certifi", + "version": "2023.7.22", + "purl": "pkg:pypi/certifi@2023.7.22", + "type": "library", + "bom-ref": "pkg:pypi/certifi@2023.7.22" + }, + { + "name": "chardet", + "version": "4.0.0", + "purl": "pkg:pypi/chardet@4.0.0", + "type": "library", + "bom-ref": "pkg:pypi/chardet@4.0.0" + }, + { + "name": "idna", + "version": "2.10", + "purl": "pkg:pypi/idna@2.10", + "type": "library", + "bom-ref": "pkg:pypi/idna@2.10" + }, + { + "name": "urllib3", + "version": "1.26.16", + "purl": "pkg:pypi/urllib3@1.26.16", + "type": "library", + "bom-ref": "pkg:pypi/urllib3@1.26.16" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/default-pip-root@0.0.0", + "dependsOn": [ + "pkg:pypi/click@8.0.4", + "pkg:pypi/flask@2.0.3", + "pkg:pypi/requests@2.25.1" + ] + }, + { + "ref": "pkg:pypi/click@8.0.4", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/flask@2.0.3", + "dependsOn": [ + "pkg:pypi/click@8.0.4", + "pkg:pypi/itsdangerous@2.0.1", + "pkg:pypi/jinja2@3.0.3", + "pkg:pypi/werkzeug@2.0.3" + ] + }, + { + "ref": "pkg:pypi/itsdangerous@2.0.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/jinja2@3.0.3", + "dependsOn": [ + "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/certifi@2023.7.22", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/chardet@4.0.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/idna@2.10", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/urllib3@1.26.16", + "dependsOn": [] + } + ] +} \ No newline at end of file diff --git a/test/providers/tst_manifests/pyproject/poetry_only_deps/poetry.lock b/test/providers/tst_manifests/pyproject/poetry_only_deps/poetry.lock new file mode 100644 index 0000000..6ff4c85 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/poetry_only_deps/poetry.lock @@ -0,0 +1,69 @@ +[[package]] +name = "flask" +version = "2.0.3" +description = "A simple framework for building complex web applications." + +[package.dependencies] +itsdangerous = ">=2.0" +Jinja2 = ">=3.0" +Werkzeug = ">=2.0" +click = ">=8.0" + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[[package]] +name = "click" +version = "8.0.4" +description = "Composable command line interface toolkit" + +[[package]] +name = "itsdangerous" +version = "2.0.1" +description = "Safely pass data to untrusted environments and back." + +[[package]] +name = "jinja2" +version = "3.0.3" +description = "A small but fast and easy to use stand-alone template engine." + +[package.dependencies] +MarkupSafe = ">=2.0" + +[[package]] +name = "werkzeug" +version = "2.0.3" +description = "The comprehensive WSGI web application library." + +[[package]] +name = "markupsafe" +version = "2.0.1" +description = "Safely add untrusted strings to HTML/XML markup." + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 3" + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" + +[[package]] +name = "urllib3" +version = "1.26.16" +description = "HTTP library with thread-safe connection pooling." diff --git a/test/providers/tst_manifests/pyproject/poetry_only_deps/pyproject.toml b/test/providers/tst_manifests/pyproject/poetry_only_deps/pyproject.toml new file mode 100644 index 0000000..1f740fe --- /dev/null +++ b/test/providers/tst_manifests/pyproject/poetry_only_deps/pyproject.toml @@ -0,0 +1,10 @@ +[tool.poetry] +name = "poetry-only-project" +version = "1.0.0" +description = "A project using only poetry dependencies" + +[tool.poetry.dependencies] +python = "^3.8" +Flask = "^2.0.3" +requests = {version = "^2.25.1", extras = ["socks"]} +click = "^8.0.4" diff --git a/test/providers/tst_manifests/pyproject/uv_lock/expected_component_sbom.json b/test/providers/tst_manifests/pyproject/uv_lock/expected_component_sbom.json new file mode 100644 index 0000000..88c5641 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/uv_lock/expected_component_sbom.json @@ -0,0 +1,48 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "default-pip-root", + "version": "0.0.0", + "purl": "pkg:pypi/default-pip-root@0.0.0", + "type": "application", + "bom-ref": "pkg:pypi/default-pip-root@0.0.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": "requests", + "version": "2.25.1", + "purl": "pkg:pypi/requests@2.25.1", + "type": "library", + "bom-ref": "pkg:pypi/requests@2.25.1" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/default-pip-root@0.0.0", + "dependsOn": [ + "pkg:pypi/flask@2.0.3", + "pkg:pypi/requests@2.25.1" + ] + }, + { + "ref": "pkg:pypi/flask@2.0.3", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/requests@2.25.1", + "dependsOn": [] + } + ] +} 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 new file mode 100644 index 0000000..6cbe35a --- /dev/null +++ b/test/providers/tst_manifests/pyproject/uv_lock/expected_stack_sbom.json @@ -0,0 +1,147 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-10-01T00:00:00.000Z", + "component": { + "name": "default-pip-root", + "version": "0.0.0", + "purl": "pkg:pypi/default-pip-root@0.0.0", + "type": "application", + "bom-ref": "pkg:pypi/default-pip-root@0.0.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": "itsdangerous", + "version": "2.0.1", + "purl": "pkg:pypi/itsdangerous@2.0.1", + "type": "library", + "bom-ref": "pkg:pypi/itsdangerous@2.0.1" + }, + { + "name": "jinja2", + "version": "3.0.3", + "purl": "pkg:pypi/jinja2@3.0.3", + "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", + "purl": "pkg:pypi/werkzeug@2.0.3", + "type": "library", + "bom-ref": "pkg:pypi/werkzeug@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": "certifi", + "version": "2023.7.22", + "purl": "pkg:pypi/certifi@2023.7.22", + "type": "library", + "bom-ref": "pkg:pypi/certifi@2023.7.22" + }, + { + "name": "chardet", + "version": "4.0.0", + "purl": "pkg:pypi/chardet@4.0.0", + "type": "library", + "bom-ref": "pkg:pypi/chardet@4.0.0" + }, + { + "name": "idna", + "version": "2.10", + "purl": "pkg:pypi/idna@2.10", + "type": "library", + "bom-ref": "pkg:pypi/idna@2.10" + }, + { + "name": "urllib3", + "version": "1.26.16", + "purl": "pkg:pypi/urllib3@1.26.16", + "type": "library", + "bom-ref": "pkg:pypi/urllib3@1.26.16" + } + ], + "dependencies": [ + { + "ref": "pkg:pypi/default-pip-root@0.0.0", + "dependsOn": [ + "pkg:pypi/flask@2.0.3", + "pkg:pypi/requests@2.25.1" + ] + }, + { + "ref": "pkg:pypi/flask@2.0.3", + "dependsOn": [ + "pkg:pypi/itsdangerous@2.0.1", + "pkg:pypi/jinja2@3.0.3", + "pkg:pypi/werkzeug@2.0.3" + ] + }, + { + "ref": "pkg:pypi/itsdangerous@2.0.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/jinja2@3.0.3", + "dependsOn": [ + "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/certifi@2023.7.22", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/chardet@4.0.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/idna@2.10", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/urllib3@1.26.16", + "dependsOn": [] + } + ] +} diff --git a/test/providers/tst_manifests/pyproject/uv_lock/pyproject.toml b/test/providers/tst_manifests/pyproject/uv_lock/pyproject.toml new file mode 100644 index 0000000..5e6f5dc --- /dev/null +++ b/test/providers/tst_manifests/pyproject/uv_lock/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "test-project" +version = "1.0.0" +dependencies = [ + "flask==2.0.3", + "requests==2.25.1", +] diff --git a/test/providers/tst_manifests/pyproject/uv_lock/uv.lock b/test/providers/tst_manifests/pyproject/uv_lock/uv.lock new file mode 100644 index 0000000..de75aa2 --- /dev/null +++ b/test/providers/tst_manifests/pyproject/uv_lock/uv.lock @@ -0,0 +1,75 @@ +version = 1 +requires-python = ">=3.8" + +[[package]] +name = "certifi" +version = "2023.7.22" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "chardet" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "flask" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug" }, +] + +[[package]] +name = "idna" +version = "2.10" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "itsdangerous" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "jinja2" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] + +[[package]] +name = "markupsafe" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "requests" +version = "2.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "chardet" }, + { name = "idna" }, + { name = "urllib3" }, +] + +[[package]] +name = "test-project" +version = "1.0.0" +source = { virtual = "." } +dependencies = [ + { name = "flask" }, + { name = "requests" }, +] + +[[package]] +name = "urllib3" +version = "1.26.16" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "werkzeug" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" }