Skip to content

Commit c0b3a87

Browse files
bugfix(cli): parse requirements.txt first and map packages to technique (CycloneDX#2030)
* parse requirements.txt first and map packages to technique Signed-off-by: Omri Yoffe <[email protected]> * lint Signed-off-by: Omri Yoffe <[email protected]> * review changes Signed-off-by: Omri Yoffe <[email protected]> * change confidence Signed-off-by: Omri Yoffe <[email protected]> * unit tests Signed-off-by: Omri Yoffe <[email protected]> * lint Signed-off-by: Omri Yoffe <[email protected]> --------- Signed-off-by: Omri Yoffe <[email protected]>
1 parent eddcc4b commit c0b3a87

File tree

3 files changed

+128
-3
lines changed

3 files changed

+128
-3
lines changed

lib/cli/index.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3620,6 +3620,7 @@ export async function createPythonBom(path, options) {
36203620
let dependencies = [];
36213621
let pkgList = [];
36223622
let formulationList = [];
3623+
const packageTechniqueMap = new Map();
36233624
const tempDir = mkdtempSync(join(getTmpDir(), "cdxgen-venv-"));
36243625
let parentComponent = createDefaultParentComponent(path, "pypi", options);
36253626
// We are checking only the root here for pipenv
@@ -3851,6 +3852,10 @@ export async function createPythonBom(path, options) {
38513852
const basePath = dirname(f);
38523853
let reqData;
38533854
let frozen = false;
3855+
3856+
reqData = readFileSync(f, { encoding: "utf-8" });
3857+
await parseReqFile(reqData, true, packageTechniqueMap);
3858+
38543859
// Attempt to pip freeze in a virtualenv to improve precision
38553860
if (options.installDeps) {
38563861
// If there are multiple requirements files then the tree is getting constructed for each one
@@ -3862,6 +3867,35 @@ export async function createPythonBom(path, options) {
38623867
parentComponent,
38633868
);
38643869
if (pkgMap.pkgList?.length) {
3870+
pkgMap.pkgList.forEach((pkg) => {
3871+
const existingTechnique = packageTechniqueMap.get(
3872+
pkg.name.toLowerCase(),
3873+
);
3874+
if (existingTechnique) {
3875+
// Update evidence to preserve original technique
3876+
if (pkg.evidence?.identity?.methods) {
3877+
pkg.evidence.identity.methods =
3878+
pkg.evidence.identity.methods.map((method) => ({
3879+
...method,
3880+
technique: existingTechnique,
3881+
}));
3882+
}
3883+
} else {
3884+
// New transitive dependency - mark as manifest-analysis derived
3885+
packageTechniqueMap.set(
3886+
pkg.name.toLowerCase(),
3887+
"manifest-analysis",
3888+
);
3889+
if (pkg.evidence?.identity?.methods) {
3890+
pkg.evidence.identity.methods =
3891+
pkg.evidence.identity.methods.map((method) => ({
3892+
...method,
3893+
technique: "manifest-analysis",
3894+
}));
3895+
}
3896+
}
3897+
});
3898+
38653899
pkgList = pkgList.concat(pkgMap.pkgList);
38663900
frozen = pkgMap.frozen;
38673901
}

lib/helpers/utils.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5628,9 +5628,14 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
56285628
*
56295629
* @param {Object} reqData Requirements.txt data
56305630
* @param {Boolean} fetchDepsInfo Fetch dependencies info from pypi
5631+
* @param {Object} packageTechniqueMap Mapping of package techniques
56315632
*/
5632-
export async function parseReqFile(reqData, fetchDepsInfo) {
5633-
const pkgList = [];
5633+
export async function parseReqFile(
5634+
reqData,
5635+
fetchDepsInfo,
5636+
packageTechniqueMap = null,
5637+
) {
5638+
let pkgList = [];
56345639
let compScope;
56355640
reqData
56365641
.replace(/\r/g, "")
@@ -5788,7 +5793,33 @@ export async function parseReqFile(reqData, fetchDepsInfo) {
57885793
}
57895794
}
57905795
});
5791-
return await getPyMetadata(pkgList, fetchDepsInfo);
5796+
const directDependencies = await getPyMetadata(pkgList, fetchDepsInfo);
5797+
if (packageTechniqueMap && directDependencies?.length) {
5798+
// Mark direct dependencies from requirements.txt as manifest-analysis
5799+
directDependencies.forEach((pkg) => {
5800+
packageTechniqueMap.set(pkg.name.toLowerCase(), "manifest-analysis");
5801+
// Also mark the evidence
5802+
if (!pkg.evidence) {
5803+
pkg.evidence = {
5804+
identity: {
5805+
field: "purl",
5806+
confidence: pkg.version ? 0.5 : 0.3,
5807+
methods: [],
5808+
},
5809+
};
5810+
}
5811+
if (!pkg.evidence.identity.methods) {
5812+
pkg.evidence.identity.methods = [];
5813+
}
5814+
pkg.evidence.identity.methods.push({
5815+
technique: "manifest-analysis",
5816+
confidence: pkg.version ? 0.5 : 0.3,
5817+
value: pkg["bom-ref"] || pkg.purl,
5818+
});
5819+
});
5820+
pkgList = pkgList.concat(directDependencies);
5821+
}
5822+
return directDependencies;
57925823
}
57935824

57945825
/**

lib/helpers/utils.test.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4780,36 +4780,69 @@ test("parseGemspecData", async () => {
47804780
});
47814781

47824782
test("parse requirements.txt", async () => {
4783+
const packageTechniqueMap1 = new Map();
47834784
let deps = await parseReqFile(
47844785
readFileSync("./test/data/requirements.comments.txt", {
47854786
encoding: "utf-8",
47864787
}),
47874788
false,
4789+
packageTechniqueMap1,
47884790
);
47894791
expect(deps.length).toEqual(31);
4792+
expect(packageTechniqueMap1.size).toEqual(31);
4793+
const packageTechniqueMap2 = new Map();
47904794
deps = await parseReqFile(
47914795
readFileSync("./test/data/requirements.freeze.txt", {
47924796
encoding: "utf-8",
47934797
}),
47944798
false,
4799+
packageTechniqueMap2,
47954800
);
47964801
expect(deps.length).toEqual(113);
4802+
expect(packageTechniqueMap2.size).toEqual(113);
47974803
expect(deps[0]).toEqual({
47984804
name: "elasticsearch",
47994805
version: "8.6.2",
48004806
scope: "required",
4807+
evidence: {
4808+
identity: {
4809+
field: "purl",
4810+
confidence: 0.5,
4811+
methods: [
4812+
{
4813+
technique: "manifest-analysis",
4814+
confidence: 0.5,
4815+
},
4816+
],
4817+
},
4818+
},
48014819
});
4820+
const packageTechniqueMap3 = new Map();
48024821
deps = await parseReqFile(
48034822
readFileSync("./test/data/chen-science-requirements.txt", {
48044823
encoding: "utf-8",
48054824
}),
48064825
false,
4826+
packageTechniqueMap3,
48074827
);
48084828
expect(deps.length).toEqual(87);
4829+
expect(packageTechniqueMap3.size).toEqual(87);
48094830
expect(deps[0]).toEqual({
48104831
name: "aiofiles",
48114832
version: "23.2.1",
48124833
scope: undefined,
4834+
evidence: {
4835+
identity: {
4836+
field: "purl",
4837+
confidence: 0.5,
4838+
methods: [
4839+
{
4840+
technique: "manifest-analysis",
4841+
confidence: 0.5,
4842+
},
4843+
],
4844+
},
4845+
},
48134846
properties: [
48144847
{
48154848
name: "cdx:pip:markers",
@@ -4818,22 +4851,49 @@ test("parse requirements.txt", async () => {
48184851
},
48194852
],
48204853
});
4854+
const packageTechniqueMap4 = new Map();
48214855
deps = await parseReqFile(
48224856
readFileSync("./test/data/requirements-lock.linux_py3.txt", {
48234857
encoding: "utf-8",
48244858
}),
48254859
false,
4860+
packageTechniqueMap4,
48264861
);
48274862
expect(deps.length).toEqual(375);
4863+
expect(packageTechniqueMap4.size).toEqual(375);
48284864
expect(deps[0]).toEqual({
48294865
name: "adal",
48304866
scope: undefined,
48314867
version: "1.2.2",
4868+
evidence: {
4869+
identity: {
4870+
field: "purl",
4871+
confidence: 0.5,
4872+
methods: [
4873+
{
4874+
technique: "manifest-analysis",
4875+
confidence: 0.5,
4876+
},
4877+
],
4878+
},
4879+
},
48324880
});
48334881
expect(deps[deps.length - 1]).toEqual({
48344882
name: "zipp",
48354883
scope: undefined,
48364884
version: "0.6.0",
4885+
evidence: {
4886+
identity: {
4887+
field: "purl",
4888+
confidence: 0.5,
4889+
methods: [
4890+
{
4891+
technique: "manifest-analysis",
4892+
confidence: 0.5,
4893+
},
4894+
],
4895+
},
4896+
},
48374897
});
48384898
});
48394899

0 commit comments

Comments
 (0)