@trustify-da/trustify-da-javascript-client 0.3.0-ea.cdf078c → 0.3.0-ea.d2fee6b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cyclone_dx_sbom.d.ts +7 -1
- package/dist/src/cyclone_dx_sbom.js +18 -5
- package/dist/src/index.d.ts +60 -2
- package/dist/src/index.js +60 -3
- package/dist/src/provider.js +2 -0
- package/dist/src/providers/base_java.d.ts +0 -9
- package/dist/src/providers/base_java.js +2 -38
- package/dist/src/providers/base_pyproject.d.ts +9 -26
- package/dist/src/providers/base_pyproject.js +50 -73
- package/dist/src/providers/golang_gomodules.d.ts +9 -0
- package/dist/src/providers/golang_gomodules.js +49 -0
- package/dist/src/providers/java_gradle.d.ts +19 -0
- package/dist/src/providers/java_gradle.js +114 -0
- package/dist/src/providers/java_maven.d.ts +8 -0
- package/dist/src/providers/java_maven.js +93 -1
- package/dist/src/providers/javascript_npm.d.ts +1 -0
- package/dist/src/providers/javascript_npm.js +21 -0
- package/dist/src/providers/javascript_pnpm.js +6 -2
- package/dist/src/providers/marker_evaluator.d.ts +14 -0
- package/dist/src/providers/marker_evaluator.js +191 -0
- package/dist/src/providers/processors/yarn_berry_processor.js +6 -2
- package/dist/src/providers/python_controller.d.ts +5 -1
- package/dist/src/providers/python_controller.js +8 -4
- package/dist/src/providers/python_pip.d.ts +4 -0
- package/dist/src/providers/python_pip.js +4 -4
- package/dist/src/providers/python_pip_pyproject.d.ts +61 -0
- package/dist/src/providers/python_pip_pyproject.js +144 -0
- package/dist/src/providers/python_poetry.d.ts +37 -4
- package/dist/src/providers/python_poetry.js +82 -13
- package/dist/src/providers/python_uv.d.ts +15 -0
- package/dist/src/providers/python_uv.js +15 -1
- package/dist/src/sbom.d.ts +7 -1
- package/dist/src/sbom.js +4 -2
- package/dist/src/tools.d.ts +26 -0
- package/dist/src/tools.js +58 -0
- package/package.json +1 -1
|
@@ -6,12 +6,13 @@ import { environmentVariableIsPopulated, getCustom, getCustomPath, invokeCommand
|
|
|
6
6
|
import Python_controller from './python_controller.js';
|
|
7
7
|
import { getParser, getIgnoreQuery, getPinnedVersionQuery } from './requirements_parser.js';
|
|
8
8
|
export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest };
|
|
9
|
-
/** @typedef {{name: string, version: string, dependencies: DependencyEntry[]}} DependencyEntry */
|
|
9
|
+
/** @typedef {{name: string, version: string, dependencies: DependencyEntry[], hashes?: Array<{alg: string, content: string}>}} DependencyEntry */
|
|
10
10
|
/**
|
|
11
11
|
* @type {string} ecosystem for python-pip is 'pip'
|
|
12
12
|
* @private
|
|
13
13
|
*/
|
|
14
14
|
const ecosystem = 'pip';
|
|
15
|
+
const NO_SCOPE = undefined;
|
|
15
16
|
/**
|
|
16
17
|
* @param {string} manifestName - the subject manifest name-type
|
|
17
18
|
* @returns {boolean} - return true if `requirements.txt` is the manifest name-type
|
|
@@ -56,7 +57,6 @@ async function provideComponent(manifest, opts = {}) {
|
|
|
56
57
|
contentType: 'application/vnd.cyclonedx+json'
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
|
-
/** @typedef {{name: string, , version: string, dependencies: DependencyEntry[]}} DependencyEntry */
|
|
60
60
|
/**
|
|
61
61
|
*
|
|
62
62
|
* @param {PackageURL}source
|
|
@@ -66,7 +66,7 @@ async function provideComponent(manifest, opts = {}) {
|
|
|
66
66
|
*/
|
|
67
67
|
function addAllDependencies(source, dep, sbom) {
|
|
68
68
|
let targetPurl = toPurl(dep["name"], dep["version"]);
|
|
69
|
-
sbom.addDependency(source, targetPurl);
|
|
69
|
+
sbom.addDependency(source, targetPurl, NO_SCOPE, dep["hashes"]);
|
|
70
70
|
let directDeps = dep["dependencies"];
|
|
71
71
|
if (directDeps !== undefined && directDeps.length > 0) {
|
|
72
72
|
directDeps.forEach((dependency) => { addAllDependencies(toPurl(dep["name"], dep["version"]), dependency, sbom); });
|
|
@@ -202,7 +202,7 @@ async function getSbomForComponentAnalysis(manifest, opts = {}) {
|
|
|
202
202
|
const license = readLicenseFromManifest(manifest);
|
|
203
203
|
sbom.addRoot(rootPurl, license);
|
|
204
204
|
dependencies.forEach(dep => {
|
|
205
|
-
sbom.addDependency(rootPurl, toPurl(dep.name, dep.version));
|
|
205
|
+
sbom.addDependency(rootPurl, toPurl(dep.name, dep.version), NO_SCOPE, dep.hashes);
|
|
206
206
|
});
|
|
207
207
|
await handleIgnoredDependencies(manifest, sbom, opts);
|
|
208
208
|
// In python there is no root component, then we must remove the dummy root we added, so the sbom json will be accepted by the DA backend
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python provider for pyproject.toml files using PEP 621 format without a lock file.
|
|
3
|
+
* Uses `pip install --dry-run --ignore-installed --report` to resolve the full dependency tree.
|
|
4
|
+
* Acts as the fallback provider when no lock file (uv.lock/poetry.lock) is found.
|
|
5
|
+
*/
|
|
6
|
+
export default class Python_pip_pyproject extends Base_pyproject {
|
|
7
|
+
/**
|
|
8
|
+
* Always returns true — pip provider is the fallback when no lock file is found.
|
|
9
|
+
* @param {string} manifestDir
|
|
10
|
+
* @param {{}} [opts={}]
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
validateLockFile(manifestDir: string, opts?: {}): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Get pip report output from env var override or by running pip.
|
|
16
|
+
* @param {string} manifestDir - directory containing pyproject.toml
|
|
17
|
+
* @param {{}} [opts={}]
|
|
18
|
+
* @returns {string} pip report JSON string
|
|
19
|
+
*/
|
|
20
|
+
_getPipReportOutput(manifestDir: string, opts?: {}): string;
|
|
21
|
+
/**
|
|
22
|
+
* Parse pip report JSON and build dependency graph.
|
|
23
|
+
* @param {string} reportJson - pip report JSON string
|
|
24
|
+
* @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}}
|
|
25
|
+
*/
|
|
26
|
+
_parsePipReport(reportJson: string): {
|
|
27
|
+
directDeps: string[];
|
|
28
|
+
graph: Map<string, {
|
|
29
|
+
name: string;
|
|
30
|
+
version: string;
|
|
31
|
+
children: string[];
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Check if a requires_dist entry is an extras-only dependency.
|
|
36
|
+
* @param {string} req - e.g. "PySocks!=1.5.7,>=1.5.6; extra == \"socks\""
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
_hasExtraMarker(req: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Extract package name from a requires_dist entry.
|
|
42
|
+
* @param {string} req - e.g. "charset_normalizer<4,>=2"
|
|
43
|
+
* @returns {string|null}
|
|
44
|
+
*/
|
|
45
|
+
_extractDepName(req: string): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve dependencies using pip install --dry-run --report.
|
|
48
|
+
* @param {string} manifestDir
|
|
49
|
+
* @param {string} _workspaceDir - unused (pip resolves from manifest directory)
|
|
50
|
+
* @param {object} parsed - parsed pyproject.toml
|
|
51
|
+
* @param {{}} [opts={}]
|
|
52
|
+
* @returns {Promise<{directDeps: string[], graph: Map}>}
|
|
53
|
+
*/
|
|
54
|
+
_getDependencyData(manifestDir: string, _workspaceDir: string, parsed: object, opts?: {}): Promise<{
|
|
55
|
+
directDeps: string[];
|
|
56
|
+
graph: Map<any, any>;
|
|
57
|
+
}>;
|
|
58
|
+
_findEggInfoDirs(dir: any): string[];
|
|
59
|
+
_cleanupEggInfo(dir: any, existing: any): void;
|
|
60
|
+
}
|
|
61
|
+
import Base_pyproject from './base_pyproject.js';
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { environmentVariableIsPopulated, getCustomPath, invokeCommand } from '../tools.js';
|
|
4
|
+
import Base_pyproject from './base_pyproject.js';
|
|
5
|
+
/**
|
|
6
|
+
* Python provider for pyproject.toml files using PEP 621 format without a lock file.
|
|
7
|
+
* Uses `pip install --dry-run --ignore-installed --report` to resolve the full dependency tree.
|
|
8
|
+
* Acts as the fallback provider when no lock file (uv.lock/poetry.lock) is found.
|
|
9
|
+
*/
|
|
10
|
+
export default class Python_pip_pyproject extends Base_pyproject {
|
|
11
|
+
/** @returns {string} */
|
|
12
|
+
_lockFileName() {
|
|
13
|
+
return '.pip-lock-nonexistent';
|
|
14
|
+
}
|
|
15
|
+
/** @returns {string} */
|
|
16
|
+
_cmdName() {
|
|
17
|
+
return 'pip';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Always returns true — pip provider is the fallback when no lock file is found.
|
|
21
|
+
* @param {string} manifestDir
|
|
22
|
+
* @param {{}} [opts={}]
|
|
23
|
+
* @returns {boolean}
|
|
24
|
+
*/
|
|
25
|
+
// eslint-disable-next-line no-unused-vars
|
|
26
|
+
validateLockFile(manifestDir, opts = {}) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get pip report output from env var override or by running pip.
|
|
31
|
+
* @param {string} manifestDir - directory containing pyproject.toml
|
|
32
|
+
* @param {{}} [opts={}]
|
|
33
|
+
* @returns {string} pip report JSON string
|
|
34
|
+
*/
|
|
35
|
+
_getPipReportOutput(manifestDir, opts) {
|
|
36
|
+
if (environmentVariableIsPopulated('TRUSTIFY_DA_PIP_REPORT')) {
|
|
37
|
+
return Buffer.from(process.env['TRUSTIFY_DA_PIP_REPORT'], 'base64').toString('ascii');
|
|
38
|
+
}
|
|
39
|
+
let pipBin = getCustomPath('pip3', opts);
|
|
40
|
+
try {
|
|
41
|
+
invokeCommand(pipBin, ['--version']);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
pipBin = getCustomPath('pip', opts);
|
|
45
|
+
}
|
|
46
|
+
let eggInfoDirs = this._findEggInfoDirs(manifestDir);
|
|
47
|
+
let result = invokeCommand(pipBin, [
|
|
48
|
+
'install', '--dry-run', '--ignore-installed', '--quiet', '--report', '-', '.'
|
|
49
|
+
], { cwd: manifestDir }).toString();
|
|
50
|
+
this._cleanupEggInfo(manifestDir, eggInfoDirs);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parse pip report JSON and build dependency graph.
|
|
55
|
+
* @param {string} reportJson - pip report JSON string
|
|
56
|
+
* @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}}
|
|
57
|
+
*/
|
|
58
|
+
_parsePipReport(reportJson) {
|
|
59
|
+
let report = JSON.parse(reportJson);
|
|
60
|
+
let packages = report.install || [];
|
|
61
|
+
let rootEntry = packages.find(p => p.download_info?.dir_info !== undefined);
|
|
62
|
+
let rootRequires = rootEntry?.metadata?.requires_dist || [];
|
|
63
|
+
let directDepNames = new Set();
|
|
64
|
+
for (let req of rootRequires) {
|
|
65
|
+
if (this._hasExtraMarker(req)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
let name = this._extractDepName(req);
|
|
69
|
+
if (name) {
|
|
70
|
+
directDepNames.add(this._canonicalize(name));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
let graph = new Map();
|
|
74
|
+
let nonRootPackages = packages.filter(p => p !== rootEntry);
|
|
75
|
+
for (let pkg of nonRootPackages) {
|
|
76
|
+
let name = pkg.metadata.name;
|
|
77
|
+
let version = pkg.metadata.version;
|
|
78
|
+
let key = this._canonicalize(name);
|
|
79
|
+
graph.set(key, { name, version, children: [] });
|
|
80
|
+
}
|
|
81
|
+
for (let pkg of nonRootPackages) {
|
|
82
|
+
let key = this._canonicalize(pkg.metadata.name);
|
|
83
|
+
let entry = graph.get(key);
|
|
84
|
+
let requires = pkg.metadata.requires_dist || [];
|
|
85
|
+
for (let req of requires) {
|
|
86
|
+
let depName = this._extractDepName(req);
|
|
87
|
+
if (!depName) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
let depKey = this._canonicalize(depName);
|
|
91
|
+
if (graph.has(depKey)) {
|
|
92
|
+
entry.children.push(depKey);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
let directDeps = [...directDepNames].filter(key => graph.has(key));
|
|
97
|
+
return { directDeps, graph };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if a requires_dist entry is an extras-only dependency.
|
|
101
|
+
* @param {string} req - e.g. "PySocks!=1.5.7,>=1.5.6; extra == \"socks\""
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
_hasExtraMarker(req) {
|
|
105
|
+
return /;\s*.*extra\s*==/.test(req);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Extract package name from a requires_dist entry.
|
|
109
|
+
* @param {string} req - e.g. "charset_normalizer<4,>=2"
|
|
110
|
+
* @returns {string|null}
|
|
111
|
+
*/
|
|
112
|
+
_extractDepName(req) {
|
|
113
|
+
let match = req.match(/^([A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)/);
|
|
114
|
+
return match ? match[1] : null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolve dependencies using pip install --dry-run --report.
|
|
118
|
+
* @param {string} manifestDir
|
|
119
|
+
* @param {string} _workspaceDir - unused (pip resolves from manifest directory)
|
|
120
|
+
* @param {object} parsed - parsed pyproject.toml
|
|
121
|
+
* @param {{}} [opts={}]
|
|
122
|
+
* @returns {Promise<{directDeps: string[], graph: Map}>}
|
|
123
|
+
*/
|
|
124
|
+
// eslint-disable-next-line no-unused-vars
|
|
125
|
+
async _getDependencyData(manifestDir, _workspaceDir, parsed, opts) {
|
|
126
|
+
let reportOutput = this._getPipReportOutput(manifestDir, opts);
|
|
127
|
+
return this._parsePipReport(reportOutput);
|
|
128
|
+
}
|
|
129
|
+
_findEggInfoDirs(dir) {
|
|
130
|
+
try {
|
|
131
|
+
return fs.readdirSync(dir).filter(f => f.endsWith('.egg-info'));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
_cleanupEggInfo(dir, existing) {
|
|
138
|
+
for (let entry of this._findEggInfoDirs(dir)) {
|
|
139
|
+
if (!existing.includes(entry)) {
|
|
140
|
+
fs.rmSync(path.join(dir, entry), { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
export default class Python_poetry extends Base_pyproject {
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} manifestDir
|
|
4
|
+
* @param {string} _workspaceDir - unused (poetry has no workspace support)
|
|
5
|
+
* @param {object} parsed - parsed pyproject.toml
|
|
6
|
+
* @param {Object} opts
|
|
7
|
+
* @returns {Promise<{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}>}
|
|
8
|
+
*/
|
|
9
|
+
_getDependencyData(manifestDir: string, _workspaceDir: string, parsed: object, opts: any): Promise<{
|
|
10
|
+
directDeps: string[];
|
|
11
|
+
graph: Map<string, {
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
children: string[];
|
|
15
|
+
}>;
|
|
16
|
+
}>;
|
|
2
17
|
/**
|
|
3
18
|
* Get poetry show --tree output.
|
|
4
19
|
* @param {string} manifestDir
|
|
20
|
+
* @param {boolean} hasDevGroup
|
|
5
21
|
* @param {Object} opts
|
|
6
22
|
* @returns {string}
|
|
7
23
|
*/
|
|
8
|
-
_getPoetryShowTreeOutput(manifestDir: string, opts: any): string;
|
|
24
|
+
_getPoetryShowTreeOutput(manifestDir: string, hasDevGroup: boolean, opts: any): string;
|
|
9
25
|
/**
|
|
10
26
|
* Get poetry show --all output (flat list with resolved versions).
|
|
11
27
|
* @param {string} manifestDir
|
|
@@ -21,16 +37,33 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
21
37
|
* @returns {Map<string, string>} canonical name -> version
|
|
22
38
|
*/
|
|
23
39
|
_parsePoetryShowAll(output: string): Map<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* Collects PEP 508 marker expressions for direct and transitive deps.
|
|
42
|
+
* Direct markers come from pyproject.toml dependency strings, e.g.:
|
|
43
|
+
* "pywin32>=311 ; sys_platform == 'win32'" → directMarkers['pywin32'] = "sys_platform == 'win32'"
|
|
44
|
+
* Transitive markers come from poetry.lock [package.dependencies] entries, e.g.:
|
|
45
|
+
* colorama = {version = "*", markers = "sys_platform == 'win32'"}
|
|
46
|
+
* → transitiveMarkers['click']['colorama'] = "sys_platform == 'win32'"
|
|
47
|
+
* @param {string|null} lockDir
|
|
48
|
+
* @param {object} parsed - parsed pyproject.toml
|
|
49
|
+
* @returns {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>}}
|
|
50
|
+
*/
|
|
51
|
+
_extractMarkerData(lockDir: string | null, parsed: object): {
|
|
52
|
+
directMarkers: Map<string, string>;
|
|
53
|
+
transitiveMarkers: Map<string, Map<string, string>>;
|
|
54
|
+
};
|
|
24
55
|
/**
|
|
25
56
|
* Parse poetry show --tree output into a dependency graph structure.
|
|
26
|
-
* Top-level lines (no indentation/tree chars) are direct deps: "name version description"
|
|
27
|
-
* Indented lines are transitive deps with tree chars: "├── name >=constraint"
|
|
28
57
|
*
|
|
29
58
|
* @param {string} treeOutput
|
|
30
59
|
* @param {Map<string, string>} versionMap - canonical name -> resolved version
|
|
60
|
+
* @param {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>}} markerData
|
|
31
61
|
* @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}}
|
|
32
62
|
*/
|
|
33
|
-
_parsePoetryTree(treeOutput: string, versionMap: Map<string, string
|
|
63
|
+
_parsePoetryTree(treeOutput: string, versionMap: Map<string, string>, markerData: {
|
|
64
|
+
directMarkers: Map<string, string>;
|
|
65
|
+
transitiveMarkers: Map<string, Map<string, string>>;
|
|
66
|
+
}): {
|
|
34
67
|
directDeps: string[];
|
|
35
68
|
graph: Map<string, {
|
|
36
69
|
name: string;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { parse as parseToml } from 'smol-toml';
|
|
3
4
|
import { environmentVariableIsPopulated, getCustom, getCustomPath, invokeCommand } from '../tools.js';
|
|
4
5
|
import Base_pyproject from './base_pyproject.js';
|
|
6
|
+
import { evaluateMarker } from './marker_evaluator.js';
|
|
5
7
|
export default class Python_poetry extends Base_pyproject {
|
|
6
8
|
/**
|
|
7
9
|
* Poetry has no native workspace/monorepo support (python-poetry/poetry#2270).
|
|
@@ -39,23 +41,31 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
39
41
|
*/
|
|
40
42
|
// eslint-disable-next-line no-unused-vars
|
|
41
43
|
async _getDependencyData(manifestDir, _workspaceDir, parsed, opts) {
|
|
42
|
-
let
|
|
44
|
+
let hasDevGroup = !!(parsed.tool?.poetry?.group?.dev || parsed.tool?.poetry?.['dev-dependencies']);
|
|
45
|
+
let treeOutput = this._getPoetryShowTreeOutput(manifestDir, hasDevGroup, opts);
|
|
43
46
|
let showAllOutput = this._getPoetryShowAllOutput(manifestDir, opts);
|
|
44
47
|
let versionMap = this._parsePoetryShowAll(showAllOutput);
|
|
45
|
-
|
|
48
|
+
let lockDir = this._findLockFileDir(manifestDir, opts);
|
|
49
|
+
let markerData = this._extractMarkerData(lockDir, parsed);
|
|
50
|
+
return this._parsePoetryTree(treeOutput, versionMap, markerData);
|
|
46
51
|
}
|
|
47
52
|
/**
|
|
48
53
|
* Get poetry show --tree output.
|
|
49
54
|
* @param {string} manifestDir
|
|
55
|
+
* @param {boolean} hasDevGroup
|
|
50
56
|
* @param {Object} opts
|
|
51
57
|
* @returns {string}
|
|
52
58
|
*/
|
|
53
|
-
_getPoetryShowTreeOutput(manifestDir, opts) {
|
|
59
|
+
_getPoetryShowTreeOutput(manifestDir, hasDevGroup, opts) {
|
|
54
60
|
if (environmentVariableIsPopulated('TRUSTIFY_DA_POETRY_SHOW_TREE')) {
|
|
55
61
|
return Buffer.from(process.env['TRUSTIFY_DA_POETRY_SHOW_TREE'], 'base64').toString('utf-8');
|
|
56
62
|
}
|
|
57
63
|
let poetryBin = getCustomPath('poetry', opts);
|
|
58
|
-
|
|
64
|
+
let args = ['show', '--tree', '--no-ansi'];
|
|
65
|
+
if (hasDevGroup) {
|
|
66
|
+
args.push('--without', 'dev');
|
|
67
|
+
}
|
|
68
|
+
return invokeCommand(poetryBin, args, { cwd: manifestDir }).toString();
|
|
59
69
|
}
|
|
60
70
|
/**
|
|
61
71
|
* Get poetry show --all output (flat list with resolved versions).
|
|
@@ -92,16 +102,60 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
92
102
|
}
|
|
93
103
|
return versions;
|
|
94
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Collects PEP 508 marker expressions for direct and transitive deps.
|
|
107
|
+
* Direct markers come from pyproject.toml dependency strings, e.g.:
|
|
108
|
+
* "pywin32>=311 ; sys_platform == 'win32'" → directMarkers['pywin32'] = "sys_platform == 'win32'"
|
|
109
|
+
* Transitive markers come from poetry.lock [package.dependencies] entries, e.g.:
|
|
110
|
+
* colorama = {version = "*", markers = "sys_platform == 'win32'"}
|
|
111
|
+
* → transitiveMarkers['click']['colorama'] = "sys_platform == 'win32'"
|
|
112
|
+
* @param {string|null} lockDir
|
|
113
|
+
* @param {object} parsed - parsed pyproject.toml
|
|
114
|
+
* @returns {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>}}
|
|
115
|
+
*/
|
|
116
|
+
_extractMarkerData(lockDir, parsed) {
|
|
117
|
+
let directMarkers = new Map();
|
|
118
|
+
let transitiveMarkers = new Map();
|
|
119
|
+
// Extract markers from PEP 621 dependency strings: "name[extras]>=ver ; marker"
|
|
120
|
+
let deps = parsed.project?.dependencies || [];
|
|
121
|
+
for (let dep of deps) {
|
|
122
|
+
let m = dep.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)\s*[^;]*;\s*(.+)$/);
|
|
123
|
+
if (m) {
|
|
124
|
+
directMarkers.set(this._canonicalize(m[1]), m[2].trim());
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (lockDir) {
|
|
128
|
+
let lockPath = path.join(lockDir, this._lockFileName());
|
|
129
|
+
if (fs.existsSync(lockPath)) {
|
|
130
|
+
let lockContent = fs.readFileSync(lockPath, 'utf-8');
|
|
131
|
+
let lock = parseToml(lockContent);
|
|
132
|
+
let packages = lock.package || [];
|
|
133
|
+
for (let pkg of packages) {
|
|
134
|
+
let pkgKey = this._canonicalize(pkg.name);
|
|
135
|
+
let pkgDeps = pkg.dependencies || {};
|
|
136
|
+
for (let [depName, depSpec] of Object.entries(pkgDeps)) {
|
|
137
|
+
let markers = typeof depSpec === 'object' && depSpec != null ? depSpec.markers : null;
|
|
138
|
+
if (markers) {
|
|
139
|
+
if (!transitiveMarkers.has(pkgKey)) {
|
|
140
|
+
transitiveMarkers.set(pkgKey, new Map());
|
|
141
|
+
}
|
|
142
|
+
transitiveMarkers.get(pkgKey).set(this._canonicalize(depName), markers);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { directMarkers, transitiveMarkers };
|
|
149
|
+
}
|
|
95
150
|
/**
|
|
96
151
|
* Parse poetry show --tree output into a dependency graph structure.
|
|
97
|
-
* Top-level lines (no indentation/tree chars) are direct deps: "name version description"
|
|
98
|
-
* Indented lines are transitive deps with tree chars: "├── name >=constraint"
|
|
99
152
|
*
|
|
100
153
|
* @param {string} treeOutput
|
|
101
154
|
* @param {Map<string, string>} versionMap - canonical name -> resolved version
|
|
155
|
+
* @param {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>}} markerData
|
|
102
156
|
* @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}}
|
|
103
157
|
*/
|
|
104
|
-
_parsePoetryTree(treeOutput, versionMap) {
|
|
158
|
+
_parsePoetryTree(treeOutput, versionMap, markerData) {
|
|
105
159
|
let lines = treeOutput.split(/\r?\n/);
|
|
106
160
|
let graph = new Map();
|
|
107
161
|
let directDeps = [];
|
|
@@ -117,6 +171,12 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
117
171
|
let name = topMatch[1];
|
|
118
172
|
let version = topMatch[2];
|
|
119
173
|
let key = this._canonicalize(name);
|
|
174
|
+
let marker = markerData.directMarkers.get(key);
|
|
175
|
+
if (marker && !evaluateMarker(marker)) {
|
|
176
|
+
currentDirectDep = null;
|
|
177
|
+
stack = [];
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
120
180
|
directDeps.push(key);
|
|
121
181
|
if (!graph.has(key)) {
|
|
122
182
|
graph.set(key, { name, version, children: [] });
|
|
@@ -143,6 +203,20 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
143
203
|
// determine depth by counting tree-drawing groups in the prefix
|
|
144
204
|
let prefix = line.substring(0, nameStart);
|
|
145
205
|
let depth = (prefix.match(/(?:[├└│ ][\s─]{2} ?)/g) || []).length;
|
|
206
|
+
// pop stack back to find the parent at depth-1
|
|
207
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
|
|
208
|
+
stack.pop();
|
|
209
|
+
}
|
|
210
|
+
let parentKey = stack.length > 0 ? stack[stack.length - 1].key : null;
|
|
211
|
+
if (parentKey) {
|
|
212
|
+
let parentMarkers = markerData.transitiveMarkers.get(parentKey);
|
|
213
|
+
if (parentMarkers) {
|
|
214
|
+
let marker = parentMarkers.get(depKey);
|
|
215
|
+
if (marker && !evaluateMarker(marker)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
146
220
|
// resolve version from the version map
|
|
147
221
|
let version = versionMap.get(depKey) || null;
|
|
148
222
|
if (!version) {
|
|
@@ -151,12 +225,7 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
151
225
|
if (!graph.has(depKey)) {
|
|
152
226
|
graph.set(depKey, { name: depName, version, children: [] });
|
|
153
227
|
}
|
|
154
|
-
|
|
155
|
-
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
|
|
156
|
-
stack.pop();
|
|
157
|
-
}
|
|
158
|
-
if (stack.length > 0) {
|
|
159
|
-
let parentKey = stack[stack.length - 1].key;
|
|
228
|
+
if (parentKey) {
|
|
160
229
|
let parentEntry = graph.get(parentKey);
|
|
161
230
|
if (parentEntry && !parentEntry.children.includes(depKey)) {
|
|
162
231
|
parentEntry.children.push(depKey);
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
export default class Python_uv extends Base_pyproject {
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} manifestDir - directory containing the target pyproject.toml
|
|
4
|
+
* @param {string} workspaceDir - workspace root (for resolving editable install paths)
|
|
5
|
+
* @param {object} parsed - parsed pyproject.toml
|
|
6
|
+
* @param {Object} opts
|
|
7
|
+
* @returns {Promise<{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}>}
|
|
8
|
+
*/
|
|
9
|
+
_getDependencyData(manifestDir: string, workspaceDir: string, parsed: object, opts: any): Promise<{
|
|
10
|
+
directDeps: string[];
|
|
11
|
+
graph: Map<string, {
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
children: string[];
|
|
15
|
+
}>;
|
|
16
|
+
}>;
|
|
2
17
|
/**
|
|
3
18
|
* Get the uv export output, either from env var or by running the command.
|
|
4
19
|
* @param {string} manifestDir
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { parse as parseToml } from 'smol-toml';
|
|
4
4
|
import { environmentVariableIsPopulated, getCustomPath, invokeCommand } from '../tools.js';
|
|
5
5
|
import Base_pyproject from './base_pyproject.js';
|
|
6
|
+
import { evaluateMarker } from './marker_evaluator.js';
|
|
6
7
|
import { getParser, getPinnedVersionQuery } from './requirements_parser.js';
|
|
7
8
|
export default class Python_uv extends Base_pyproject {
|
|
8
9
|
/** @returns {string} */
|
|
@@ -36,7 +37,7 @@ export default class Python_uv extends Base_pyproject {
|
|
|
36
37
|
return Buffer.from(process.env['TRUSTIFY_DA_UV_EXPORT'], 'base64').toString('ascii');
|
|
37
38
|
}
|
|
38
39
|
let uvBin = getCustomPath('uv', opts);
|
|
39
|
-
return invokeCommand(uvBin, ['export', '--format', 'requirements.txt', '--frozen', '--no-hashes'], { cwd: manifestDir }).toString();
|
|
40
|
+
return invokeCommand(uvBin, ['export', '--format', 'requirements.txt', '--frozen', '--no-hashes', '--no-dev', '--no-emit-project'], { cwd: manifestDir }).toString();
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
42
43
|
* Parse uv export output into a dependency graph using tree-sitter-requirements
|
|
@@ -70,6 +71,9 @@ export default class Python_uv extends Base_pyproject {
|
|
|
70
71
|
let version = memberParsed.project?.version || memberParsed.tool?.poetry?.version;
|
|
71
72
|
if (name && version) {
|
|
72
73
|
let key = this._canonicalize(name);
|
|
74
|
+
if (key === canonProjectName) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
73
77
|
currentPkg = { name, version, parents: new Set() };
|
|
74
78
|
packages.set(key, currentPkg);
|
|
75
79
|
collectingVia = false;
|
|
@@ -86,6 +90,16 @@ export default class Python_uv extends Base_pyproject {
|
|
|
86
90
|
if (!nameNode) {
|
|
87
91
|
continue;
|
|
88
92
|
}
|
|
93
|
+
// Skip packages with non-matching PEP 508 markers, e.g. "pywin32==311 ; sys_platform == 'win32'"
|
|
94
|
+
let markerNode = child.children.find(c => c.type === 'marker_spec');
|
|
95
|
+
if (markerNode) {
|
|
96
|
+
let markerText = markerNode.text.replace(/^\s*;\s*/, '');
|
|
97
|
+
if (!evaluateMarker(markerText)) {
|
|
98
|
+
currentPkg = null;
|
|
99
|
+
collectingVia = false;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
89
103
|
let name = nameNode.text;
|
|
90
104
|
let version = null;
|
|
91
105
|
let versionMatches = pinnedVersionQuery.matches(child);
|
package/dist/src/sbom.d.ts
CHANGED
|
@@ -25,9 +25,14 @@ export default class Sbom {
|
|
|
25
25
|
/**
|
|
26
26
|
* @param {component} sourceRef current source Component ( Starting from root component by clients)
|
|
27
27
|
* @param {PackageURL} targetRef current dependency to add to Dependencies list of component sourceRef
|
|
28
|
+
* @param {string} [scope] - Scope of the dependency
|
|
29
|
+
* @param {Array<{alg: string, content: string}>} [targetHashes] - Optional hashes for the target component
|
|
28
30
|
* @return Sbom
|
|
29
31
|
*/
|
|
30
|
-
addDependency(sourceRef: component, targetRef: PackageURL, scope
|
|
32
|
+
addDependency(sourceRef: component, targetRef: PackageURL, scope?: string, targetHashes?: Array<{
|
|
33
|
+
alg: string;
|
|
34
|
+
content: string;
|
|
35
|
+
}>): CycloneDxSbom;
|
|
31
36
|
/**
|
|
32
37
|
* @return String sbom json in a string format
|
|
33
38
|
*/
|
|
@@ -45,6 +50,7 @@ export default class Sbom {
|
|
|
45
50
|
version: any;
|
|
46
51
|
scope: any;
|
|
47
52
|
licenses?: any;
|
|
53
|
+
hashes?: any;
|
|
48
54
|
};
|
|
49
55
|
/** This method gets a component object, and a string name, and checks if the name is a substring of the component' purl.
|
|
50
56
|
* @param {} component to search in its dependencies
|
package/dist/src/sbom.js
CHANGED
|
@@ -43,10 +43,12 @@ export default class Sbom {
|
|
|
43
43
|
/**
|
|
44
44
|
* @param {component} sourceRef current source Component ( Starting from root component by clients)
|
|
45
45
|
* @param {PackageURL} targetRef current dependency to add to Dependencies list of component sourceRef
|
|
46
|
+
* @param {string} [scope] - Scope of the dependency
|
|
47
|
+
* @param {Array<{alg: string, content: string}>} [targetHashes] - Optional hashes for the target component
|
|
46
48
|
* @return Sbom
|
|
47
49
|
*/
|
|
48
|
-
addDependency(sourceRef, targetRef, scope) {
|
|
49
|
-
return this.sbomModel.addDependency(sourceRef, targetRef, scope);
|
|
50
|
+
addDependency(sourceRef, targetRef, scope, targetHashes) {
|
|
51
|
+
return this.sbomModel.addDependency(sourceRef, targetRef, scope, targetHashes);
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
52
54
|
* @return String sbom json in a string format
|
package/dist/src/tools.d.ts
CHANGED
|
@@ -61,6 +61,32 @@ export function toPurlFromString(strPurl: any): PackageURL | null;
|
|
|
61
61
|
* @param {string} cwd - directory for which to find the root of the git repository.
|
|
62
62
|
*/
|
|
63
63
|
export function getGitRootDir(cwd: string): string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Normalize a filesystem path, lowercasing on Windows for case-insensitive comparison.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} thePath
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
export function normalizePath(thePath: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Walk up from `startDir` to `repoRoot` looking for an executable wrapper script.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} startDir - Absolute directory to start from
|
|
75
|
+
* @param {string} wrapperName - Wrapper filename (e.g. `mvnw`, `gradlew`)
|
|
76
|
+
* @param {string} [repoRoot] - Stop boundary (defaults to git root or filesystem root)
|
|
77
|
+
* @returns {string | undefined}
|
|
78
|
+
*/
|
|
79
|
+
export function traverseForWrapper(startDir: string, wrapperName: string, repoRoot?: string): string | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a build-tool binary, preferring a wrapper when configured.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} globalBinary - Global binary name (e.g. `mvn`, `gradle`)
|
|
84
|
+
* @param {string} localWrapper - Wrapper filename (e.g. `mvnw`, `gradlew.bat`)
|
|
85
|
+
* @param {string} startDir - Directory from which to start the wrapper search
|
|
86
|
+
* @param {import('./index.js').Options} [opts={}]
|
|
87
|
+
* @returns {string} Path to the resolved binary
|
|
88
|
+
*/
|
|
89
|
+
export function resolveBinary(globalBinary: string, localWrapper: string, startDir: string, opts?: import("./index.js").Options): string;
|
|
64
90
|
/** this method invokes command string in a process in a synchronous way.
|
|
65
91
|
* @param {string} bin - the command to be invoked
|
|
66
92
|
* @param {Array<string>} args - the args to pass to the binary
|