Skip to content

Commit 481b05e

Browse files
committed
feat: Move to lib and add tests
1 parent af88914 commit 481b05e

File tree

5 files changed

+155
-77
lines changed

5 files changed

+155
-77
lines changed

index.js

Lines changed: 4 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ const glob = require('glob');
55

66
const toolkit = require('@github/dependency-submission-toolkit');
77

8+
const { getManifestsFromSpdxFiles, searchFiles } = require('./lib');
9+
810
async function run() {
911
let manifests = getManifestsFromSpdxFiles(searchFiles());
1012

1113
let snapshot = new toolkit.Snapshot({
1214
name: "spdx-to-dependency-graph-action",
13-
version: "0.0.1",
15+
version: "0.1.1",
1416
url: "https://github.com/advanced-security/spdx-dependency-submission-action",
1517
},
1618
github.context,
@@ -26,74 +28,4 @@ async function run() {
2628
toolkit.submitSnapshot(snapshot);
2729
}
2830

29-
function getManifestFromSpdxFile(document, fileName) {
30-
core.debug(`getManifestFromSpdxFile processing ${fileName}`);
31-
32-
let manifest = new toolkit.Manifest(document.name, fileName);
33-
34-
core.debug(`Processing ${document.packages?.length} packages`);
35-
36-
document.packages?.forEach(pkg => {
37-
let packageName = pkg.name;
38-
let packageVersion = pkg.packageVersion;
39-
let referenceLocator = pkg.externalRefs?.find(ref => ref.referenceCategory === "PACKAGE-MANAGER" && ref.referenceType === "purl")?.referenceLocator;
40-
let genericPurl = `pkg:generic/${packageName}@${packageVersion}`;
41-
// SPDX 2.3 defines a purl field
42-
let purl;
43-
if (pkg.purl != undefined) {
44-
purl = pkg.purl;
45-
} else if (referenceLocator != undefined) {
46-
purl = referenceLocator;
47-
} else {
48-
purl = genericPurl;
49-
}
50-
51-
// Working around weird encoding issues from an SBOM generator
52-
// Find the last instance of %40 and replace it with @
53-
purl = replaceVersionEscape(purl);
54-
55-
let relationships = document.relationships?.find(rel => rel.relatedSpdxElement == pkg.SPDXID && rel.relationshipType == "DEPENDS_ON" && rel.spdxElementId != "SPDXRef-RootPackage");
56-
if (relationships != null && relationships.length > 0) {
57-
manifest.addIndirectDependency(new toolkit.Package(purl));
58-
} else {
59-
manifest.addDirectDependency(new toolkit.Package(purl));
60-
}
61-
});
62-
return manifest;
63-
}
64-
65-
function getManifestsFromSpdxFiles(files) {
66-
core.debug(`Processing ${files.length} files`);
67-
let manifests = [];
68-
files?.forEach(file => {
69-
core.debug(`Processing ${file}`);
70-
manifests.push(getManifestFromSpdxFile(JSON.parse(fs.readFileSync(file)), file));
71-
});
72-
return manifests;
73-
}
74-
75-
function searchFiles() {
76-
let filePath = core.getInput('filePath');
77-
let filePattern = core.getInput('filePattern');
78-
79-
return glob.sync(`${filePath}/${filePattern}`, {});
80-
}
81-
82-
// Fixes issues with an escaped version string
83-
function replaceVersionEscape(purl) {
84-
// Some tools are failing to escape the namespace, so we will escape it to work around that
85-
// @ -> %40
86-
// ^ -> %5E
87-
purl = purl.replace("/@", "/%40").replaceAll("^", "%5E");
88-
89-
//If there's an "@" in the purl, then we don't need to do anything.
90-
if (purl != null && purl != undefined && !purl?.includes("@")) {
91-
let index = purl.lastIndexOf("%40");
92-
if (index > 0) {
93-
purl = purl.substring(0, index) + "@" + purl.substring(index + 3);
94-
}
95-
}
96-
return purl;
97-
}
98-
99-
run();
31+
run();

lib/index.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
const core = require('@actions/core');
2+
const github = require('@actions/github');
3+
const fs = require('fs');
4+
const glob = require('glob');
5+
6+
const toolkit = require('@github/dependency-submission-toolkit');
7+
8+
/**
9+
* Extracts and constructs a manifest object from an SPDX document for a given file.
10+
* This function processes an SPDX document, iterating over its packages to construct a manifest.
11+
* It handles package information, including name, version, and package URLs (purls), and categorizes packages as direct or indirect dependencies based on their relationships.
12+
* Special handling is applied to package URLs to work around encoding issues, using the `replaceVersionEscape` function.
13+
*
14+
* @param {Object} document - The SPDX document object containing package and relationship data.
15+
* @param {string} fileName - The name of the file from which the SPDX document was extracted.
16+
* @returns {Object} A manifest object containing the processed package data, including direct and indirect dependencies.
17+
*/
18+
module.exports.getManifestFromSpdxFile = (document, fileName) => {
19+
core.debug(`getManifestFromSpdxFile processing ${fileName}`);
20+
21+
let manifest = new toolkit.Manifest(document.name, fileName);
22+
23+
core.debug(`Processing ${document.packages?.length} packages`);
24+
25+
document.packages?.forEach(pkg => {
26+
let packageName = pkg.name;
27+
let packageVersion = pkg.packageVersion;
28+
let referenceLocator = pkg.externalRefs?.find(ref => ref.referenceCategory === "PACKAGE-MANAGER" && ref.referenceType === "purl")?.referenceLocator;
29+
let genericPurl = `pkg:generic/${packageName}@${packageVersion}`;
30+
// SPDX 2.3 defines a purl field
31+
let purl;
32+
if (pkg.purl != undefined) {
33+
purl = pkg.purl;
34+
} else if (referenceLocator != undefined) {
35+
purl = referenceLocator;
36+
} else {
37+
purl = genericPurl;
38+
}
39+
40+
try {
41+
// Working around weird encoding issues from an SBOM generator
42+
// Find the last instance of %40 and replace it with @
43+
purl = replaceVersionEscape(purl);
44+
45+
let relationships = document.relationships?.find(rel => rel.relatedSpdxElement == pkg.SPDXID && rel.relationshipType == "DEPENDS_ON" && rel.spdxElementId != "SPDXRef-RootPackage");
46+
if (relationships != null && relationships.length > 0) {
47+
manifest.addIndirectDependency(new toolkit.Package(purl));
48+
} else {
49+
manifest.addDirectDependency(new toolkit.Package(purl));
50+
}
51+
}
52+
catch (error) {
53+
core.warning(`Error processing package "${packageName}@${packageVersion}" in ${fileName}`);
54+
core.warning(error);
55+
}
56+
});
57+
return manifest;
58+
}
59+
60+
/**
61+
* Extracts manifest data from SPDX files.
62+
* Iterates over an array of SPDX file paths, reads each file, parses its JSON content, and then extracts the manifest data using `getManifestFromSpdxFile`.
63+
* Each manifest is collected and returned in an array.
64+
*
65+
* @param {string[]} files - An array of file paths pointing to SPDX files.
66+
* @returns {Object[]} An array of manifest objects extracted from the SPDX files.
67+
*/
68+
module.exports.getManifestsFromSpdxFiles = (files) => {
69+
core.debug(`Processing ${files.length} files`);
70+
let manifests = [];
71+
files?.forEach(file => {
72+
core.debug(`Processing ${file}`);
73+
manifests.push(getManifestFromSpdxFile(JSON.parse(fs.readFileSync(file)), file));
74+
});
75+
return manifests;
76+
}
77+
78+
/**
79+
* Searches for files matching a specified pattern within a given file path.
80+
* Utilizes the `glob` module to perform the search, returning an array of matching file paths.
81+
*
82+
* @returns {string[]} An array of strings representing the paths of files that match the given pattern within the specified path.
83+
*/
84+
module.exports.searchFiles = () => {
85+
let filePath = core.getInput('filePath');
86+
let filePattern = core.getInput('filePattern');
87+
88+
return glob.sync(`${filePath}/${filePattern}`, {});
89+
}
90+
91+
/**
92+
* Escapes certain characters in a package URL (purl) to work around issues with some tools not escaping namespaces correctly.
93+
* Specifically, it replaces "@" with "%40" and "^" with "%5E". If an "@" is already present in the purl, it assumes no further action is needed.
94+
* If a "%40" is present in the purl without an "@", it converts the last occurrence of "%40" back to "@".
95+
*
96+
* @param {string} purl - The package URL to be processed.
97+
* @returns {string} The processed package URL with the necessary characters escaped or unescaped.
98+
*/
99+
module.exports.replaceVersionEscape = (purl) => {
100+
// Some tools are failing to escape the namespace, so we will escape it to work around that
101+
// @ -> %40
102+
// ^ -> %5E
103+
purl = purl.replace("/@", "/%40").replaceAll("^", "%5E");
104+
105+
//If there's an "@" in the purl, then we don't need to do anything.
106+
if (purl != null && purl != undefined && !purl?.includes("@")) {
107+
let index = purl.lastIndexOf("%40");
108+
if (index > 0) {
109+
purl = purl.substring(0, index) + "@" + purl.substring(index + 3);
110+
}
111+
}
112+
return purl;
113+
}

lib/index.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const lib = require('./index');
2+
const packageurl = require('packageurl-js');
3+
4+
describe("replace version escape", () => {
5+
test("replace @ in namespace", () => {
6+
// https://www.npmjs.com/package/@angular/cli
7+
const purl = "pkg:NPM/@angular/[email protected]";
8+
9+
var new_purl = lib.replaceVersionEscape(purl);
10+
expect(new_purl).toBe("pkg:NPM/%40angular/[email protected]");
11+
packageurl.PackageURL.fromString(new_purl);
12+
});
13+
test("if encoding ha already happened", () => {
14+
const purl = "pkg:npm/es-abstract%401.16.0";
15+
16+
var new_purl = lib.replaceVersionEscape(purl);
17+
expect(new_purl).toBe("pkg:npm/[email protected]");
18+
packageurl.PackageURL.fromString(new_purl);
19+
20+
const purl2 = "pkg:npm/%40vue/cli-shared-utils%404.0.4";
21+
var new_purl2 = lib.replaceVersionEscape(purl2);
22+
expect(new_purl2).toBe("pkg:npm/%40vue/[email protected]");
23+
packageurl.PackageURL.fromString(new_purl2);
24+
25+
})
26+
test("replace ^ in version", () => {
27+
const purl = "pkg:NPM/@angular/cli@^4.17.21";
28+
29+
var new_purl = lib.replaceVersionEscape(purl);
30+
expect(new_purl).toBe("pkg:NPM/%40angular/cli@%5E4.17.21");
31+
packageurl.PackageURL.fromString(new_purl);
32+
})
33+
})

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
"@github/dependency-submission-toolkit": "2.0.4"
3232
},
3333
"devDependencies": {
34-
"eslint": "^8.40.0",
3534
"@vercel/ncc": "^0.36.1",
36-
"jest": "^29.4.2"
35+
"eslint": "^8.40.0",
36+
"jest": "^29.7.0"
3737
}
3838
}

0 commit comments

Comments
 (0)