Skip to content

Commit

Permalink
cdxgen interactive repl (#458)
Browse files Browse the repository at this point in the history
* cdxgen interactive repl

Signed-off-by: Prabhu Subramanian <[email protected]>

* Added sort command

Signed-off-by: Prabhu Subramanian <[email protected]>

* Added save and update command

Signed-off-by: Prabhu Subramanian <[email protected]>

* Remove swift action on windows

Signed-off-by: Prabhu Subramanian <[email protected]>

* Remove swift action on windows

Signed-off-by: Prabhu Subramanian <[email protected]>

* Remove swift action on windows

Signed-off-by: Prabhu Subramanian <[email protected]>

* Don't run docker tests in Windows on CI

Signed-off-by: Caroline Russell <[email protected]>

* Specify shell instead of separate windows tests

Signed-off-by: Prabhu Subramanian <[email protected]>

* Bump version

Signed-off-by: Prabhu Subramanian <[email protected]>

---------

Signed-off-by: Prabhu Subramanian <[email protected]>
Signed-off-by: Caroline Russell <[email protected]>
Co-authored-by: Caroline Russell <[email protected]>
  • Loading branch information
prabhu and cerrussell authored Aug 13, 2023
1 parent 21feaf6 commit c3c1d95
Show file tree
Hide file tree
Showing 8 changed files with 392 additions and 15 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/repotests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jobs:
env:
CI: true
- uses: swift-actions/setup-swift@v1
if: matrix.os == 'ubuntu-latest'
- uses: actions/checkout@v3
with:
repository: 'ShiftLeftSecurity/shiftleft-java-example'
Expand Down Expand Up @@ -136,6 +137,7 @@ jobs:
# mv *.hpi jenkins
# CDXGEN_DEBUG_MODE=debug bin/cdxgen.js -p -r -t jenkins jenkins -o bomresults/bom-jenkins.json --validate
ls -ltr bomresults
shell: bash
- name: repotests 1.4
run: |
bin/cdxgen.js -p -r -t java repotests/shiftleft-java-example -o bomresults/bom-java.json --generate-key-and-sign --spec-version 1.4
Expand All @@ -147,6 +149,7 @@ jobs:
FETCH_LICENSE=0 bin/cdxgen.js -p -r repotests/Goatly.NET -o bomresults/bom-csharp3.json --validate --spec-version 1.4
FETCH_LICENSE=true bin/cdxgen.js -p -r -t python repotests/DjanGoat -o bomresults/bom-python.json --validate --spec-version 1.4
bin/cdxgen.js -p -r -t php repotests/Vulnerable-Web-Application -o bomresults/bom-php.json --validate --spec-version 1.4
shell: bash
- name: denotests
if: github.ref == 'refs/heads/master' && matrix.os == 'ubuntu-latest'
run: |
Expand Down
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,50 @@ const bomNSData = await createBom(filePath, options);
const dbody = await submitBom(args, bomNSData.bomJson);
```

## Interactive mode

`cdxi` is a new interactive REPL server to interactively create, import and search an SBoM. All the exported functions from cdxgen and node.js could be used in this mode. In addition, several custom commands are defined.

### Custom commands

| Command | Description |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| .create | Create an SBoM from a path |
| .import | Import an existing SBoM from a path. Any SBoM in CycloneDX format is supported. |
| .search | Search the given string in the components name, group, purl and description |
| .sort | Sort the components based on the given attribute. Eg: .sort name to sort by name. Accepts full jsonata [order by](http://docs.jsonata.org/path-operators#order-by-) clause too. Eg: `.sort components^(>name)` |
| .query | Pass a raw query in [jsonata](http://docs.jsonata.org/) format |
| .print | Print the SBoM as a table |
| .tree | Print the dependency tree if available |
| .validate | Validate the SBoM |
| .exit | To exit the shell |
| .save | To save the modified SBoM to a new file |
| .update | Update components based on query expression. Use syntax `\| query \| new object \|`. See example. |

### Sample REPL usage

Start the REPL server.

```shell
cdxi
```

Below are some example commands to create an SBoM for a spring application and perform searches and queries.

```
.create /mnt/work/vuln-spring
.print
.search spring
.query components[name ~> /spring/ and scope = "required"]
.sort name
.sort components^(>name)
.update | components[name ~> /spring/] | {'publisher': "foo"} |
```

### REPL History

Repl history will get persisted under `$HOME/.config/.cdxgen` directory. To override this location, use the environment variable `CDXGEN_REPL_HISTORY`.

## Node.js >= 20 permission model

Refer to the [permissions document](./docs/PERMISSIONS.md)
Expand Down
311 changes: 311 additions & 0 deletions bin/repl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
import repl from "node:repl";
import jsonata from "jsonata";
import fs from "node:fs";
import { join } from "node:path";
import { homedir, tmpdir } from "node:os";
import process from "node:process";

import { createBom } from "../index.js";
import { validateBom } from "../validator.js";
import { printTable, printDependencyTree } from "../display.js";

const options = {
useColors: true,
breakEvalOnSigint: true,
preview: true,
prompt: "↝ ",
ignoreUndefined: true,
useGlobal: true
};

const cdxArt = ` ██████╗██████╗ ██╗ ██╗
██╔════╝██╔══██╗╚██╗██╔╝
██║ ██║ ██║ ╚███╔╝
██║ ██║ ██║ ██╔██╗
╚██████╗██████╔╝██╔╝ ██╗
╚═════╝╚═════╝ ╚═╝ ╚═╝
`;

console.log("\n" + cdxArt);

// The current sbom is stored here
let sbom = undefined;

let historyFile = undefined;
const historyConfigDir = join(homedir(), ".config", ".cdxgen");
if (!process.env.CDXGEN_REPL_HISTORY && !fs.existsSync(historyConfigDir)) {
try {
fs.mkdirSync(historyConfigDir, { recursive: true });
historyFile = join(historyConfigDir, ".repl_history");
} catch (e) {
// ignore
}
} else {
historyFile = join(historyConfigDir, ".repl_history");
}

export const importSbom = (sbomOrPath) => {
if (sbomOrPath && sbomOrPath.endsWith(".json") && fs.existsSync(sbomOrPath)) {
try {
sbom = JSON.parse(fs.readFileSync(sbomOrPath, "utf-8"));
console.log(`✅ SBoM imported successfully from ${sbomOrPath}`);
} catch (e) {
console.log(
`⚠ Unable to import the SBoM from ${sbomOrPath} due to ${e}`
);
}
} else {
console.log(`⚠ ${sbomOrPath} is invalid.`);
}
};
// Load any sbom passed from the command line
if (process.argv.length > 2) {
importSbom(process.argv[process.argv.length - 1]);
console.log("💭 Type .print to view the SBoM as a table");
} else if (fs.existsSync("bom.json")) {
// If the current directory has a bom.json load it
importSbom("bom.json");
} else {
console.log("💭 Use .create <path> to create an SBoM for the given path.");
console.log("💭 Use .import <json> to import an existing SBoM.");
console.log("💭 Type .exit or press ctrl+d to close.");
}

const cdxgenRepl = repl.start(options);
if (historyFile) {
cdxgenRepl.setupHistory(
process.env.CDXGEN_REPL_HISTORY || historyFile,
(err) => {
if (err) {
console.log(
"⚠ REPL history would not be persisted for this session. Set the environment variable CDXGEN_REPL_HISTORY to specify a custom history file"
);
}
}
);
}
cdxgenRepl.defineCommand("create", {
help: "create an SBoM for the given path",
async action(sbomOrPath) {
this.clearBufferedCommand();
const tempDir = fs.mkdtempSync(join(tmpdir(), "cdxgen-repl-"));
const bomFile = join(tempDir, "bom.json");
const bomNSData = await createBom(sbomOrPath, {
multiProject: true,
installDeps: true,
output: bomFile
});
if (bomNSData) {
sbom = bomNSData.bomJson;
console.log("✅ SBoM imported successfully.");
console.log("💭 Type .print to view the SBoM as a table");
} else {
console.log("SBoM was not generated successfully");
}
this.displayPrompt();
}
});
cdxgenRepl.defineCommand("import", {
help: "import an existing SBoM",
action(sbomOrPath) {
this.clearBufferedCommand();
importSbom(sbomOrPath);
this.displayPrompt();
}
});
cdxgenRepl.defineCommand("exit", {
help: "exit",
action() {
this.close();
}
});
cdxgenRepl.defineCommand("sbom", {
help: "show the current sbom",
action() {
if (sbom) {
console.log(sbom);
} else {
console.log(
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
);
}
this.displayPrompt();
}
});
cdxgenRepl.defineCommand("search", {
help: "search the current sbom",
async action(searchStr) {
if (sbom) {
if (searchStr) {
try {
if (!searchStr.includes("~>")) {
searchStr = `components[group ~> /${searchStr}/i or name ~> /${searchStr}/i or description ~> /${searchStr}/i or publisher ~> /${searchStr}/i or purl ~> /${searchStr}/i]`;
}
const expression = jsonata(searchStr);
let components = await expression.evaluate(sbom);
if (!components) {
console.log("No results found!");
} else {
printTable({ components, dependencies: [] });
}
} catch (e) {
console.log(e);
}
} else {
console.log(
"⚠ Specify the search string. Eg: .search <search string>"
);
}
} else {
console.log(
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
);
}
this.displayPrompt();
}
});
cdxgenRepl.defineCommand("sort", {
help: "sort the current sbom based on the attribute",
async action(sortStr) {
if (sbom) {
if (sortStr) {
try {
if (!sortStr.includes("^")) {
sortStr = `components^(${sortStr})`;
}
const expression = jsonata(sortStr);
let components = await expression.evaluate(sbom);
if (!components) {
console.log("No results found!");
} else {
printTable({ components, dependencies: [] });
// Store the sorted list in memory
if (components.length === sbom.components.length) {
sbom.components = components;
}
}
} catch (e) {
console.log(e);
}
} else {
console.log("⚠ Specify the attribute to sort by. Eg: .sort name");
}
} else {
console.log(
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
);
}
this.displayPrompt();
}
});
cdxgenRepl.defineCommand("query", {
help: "query the current sbom",
async action(querySpec) {
if (sbom) {
if (querySpec) {
try {
const expression = jsonata(querySpec);
console.log(await expression.evaluate(sbom));
} catch (e) {
console.log(e);
}
} else {
console.log(
"⚠ Specify the search specification in jsonata format. Eg: .query metadata.component"
);
}
} else {
console.log(
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
);
}
this.displayPrompt();
}
});
cdxgenRepl.defineCommand("print", {
help: "print the current sbom as a table",
action() {
if (sbom) {
printTable(sbom);
} else {
console.log(
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
);
}
this.displayPrompt();
}
});
cdxgenRepl.defineCommand("tree", {
help: "display the dependency tree",
action() {
if (sbom) {
printDependencyTree(sbom);
} else {
console.log(
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
);
}
this.displayPrompt();
}
});
cdxgenRepl.defineCommand("validate", {
help: "validate the sbom",
action() {
if (sbom) {
const result = validateBom(sbom);
if (result) {
console.log("SBoM is valid!");
}
} else {
console.log(
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
);
}
this.displayPrompt();
}
});
cdxgenRepl.defineCommand("save", {
help: "save the sbom to a new file",
action(saveToFile) {
if (sbom) {
if (!saveToFile) {
saveToFile = "bom.json";
}
fs.writeFileSync(saveToFile, JSON.stringify(sbom, null, 2));
console.log(`SBoM saved successfully to ${saveToFile}`);
} else {
console.log(
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
);
}
this.displayPrompt();
}
});
cdxgenRepl.defineCommand("update", {
help: "update the sbom components based on the given query",
async action(updateSpec) {
if (sbom) {
if (!updateSpec) {
return;
}
if (!updateSpec.startsWith("|")) {
updateSpec = "|" + updateSpec;
}
if (!updateSpec.endsWith("|")) {
updateSpec = updateSpec + "|";
}
updateSpec = "$ ~> " + updateSpec;
const expression = jsonata(updateSpec);
const newSbom = await expression.evaluate(sbom);
if (newSbom && newSbom.components.length <= sbom.components.length) {
sbom = newSbom;
}
console.log("SBoM updated successfully.");
} else {
console.log(
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
);
}
this.displayPrompt();
}
});
3 changes: 3 additions & 0 deletions display.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const MAX_TREE_DEPTH = 3;

export const printTable = (bomJson) => {
const data = [["Group", "Name", "Version", "Scope"]];
if (!bomJson || !bomJson.components) {
return;
}
for (const comp of bomJson.components) {
data.push([comp.group || "", comp.name, comp.version, comp.scope || ""]);
}
Expand Down
Loading

0 comments on commit c3c1d95

Please sign in to comment.