| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- import yargs from "yargs/yargs";
- import path from "path";
- import fetch from "node-fetch";
- import { hideBin } from "yargs/helpers";
- type FormatValue = "tree" | "csv";
- type Format = { header: string, apply: (node: PackageLockTree, parents: string[], audit: AuditResult) => string }
- type FilterValue = "none" | "oracle";
- type Filter = (node: PackageLockTree, parents: string[]) => boolean;
- type Audit = (node: PackageLockTree) => Promise<AuditResult>;
- const args = yargs(hideBin(process.argv))
- .option("package", {
- type: "string",
- describe: "The full or relative path to the package.json file",
- require: true,
- coerce: path.resolve
- })
- .option("format", {
- type: "string",
- describe: "Possible values are 'tree' and 'csv'",
- default: "csv",
- coerce: (value: FormatValue): Format =>
- value === "tree"
- ? {
- header: "",
- apply: (node, parents, audit) =>
- `${" ".repeat(parents.length)}${node.value.name}@${node.value.version}${node.value.dev ? " (dev)" : ""} audit:${audit.status}`
- }
- : {
- header: "name,version,root,parent,type,audit,expiry date,registered version,registered expiry date,last expired version,last expired expiry date",
- apply: (node, parents, audit) =>
- `${node.value.name},${node.value.version},${parents[0] ?? ""},${parents[parents.length - 1] ?? ""},${node.value.dev ? "devDependency" : "dependency"}` +
- `,${audit.status},${audit.expiryDate ?? ""},${audit.registeredVersion ?? ""},${audit.registeredExpiryDate ?? ""},${audit.lastExpiredVersion ?? ""},${audit.lastExpiredExpiryDate ?? ""}`
- }
- })
- .option("filter", {
- type: "string",
- describe: "Possible values are 'none' and 'oracle'",
- default: "oracle",
- coerce: (value: FilterValue): Filter =>
- value === "oracle"
- ? (node, parents) => (parents.length === 0 && !node.value.name.startsWith("@os")) || (!node.value.name.startsWith("@o") && parents.length > 0 && parents.every(p => p.startsWith("@os")))
- : (_, __) => true
- })
- .option("audit", {
- type: "boolean",
- describe: "Open Source Compliance Service audit",
- default: false,
- coerce: (value: boolean): Audit =>
- value
- ? node => auditTreeNode(node)
- : _ => Promise.resolve({ status: "n/a" })
- })
- .help()
- .argv;
- interface Dictionary<T> {
- [index: string]: T
- }
- interface Package {
- dependencies: Dictionary<string>;
- devDependencies?: Dictionary<string>;
- }
- type PackageLockDependencies = Dictionary<{
- version: string;
- dev: boolean;
- requires?: Dictionary<string>;
- dependencies?: PackageLockDependencies;
- }>
- interface PackageLock {
- dependencies: PackageLockDependencies;
- }
- type Tree<T> = Leaf<T> | Node<T>;
- type Node<T> = {
- type: "node";
- value: T;
- children: Tree<T>[];
- }
- type Leaf<T> = {
- type: "leaf";
- value: T;
- }
- type PackageLockTree = Tree<{ name: string, version: string, dev: boolean }>;
- const constructDepTree = (packageName: string, context: PackageLockDependencies, stack: string[] = []): PackageLockTree => {
- const dependency = context[packageName];
- const value = { name: packageName, version: dependency.version, dev: dependency.dev };
- if (!dependency.requires) {
- return { type: "leaf", value };
- }
- return {
- type: "node",
- value,
- children: Object.keys(dependency.requires).filter(k => stack.every(s => s !== k))
- .map(requiredDepName => constructDepTree(requiredDepName, { ...context, ...dependency.dependencies }, [...stack, packageName]))
- };
- }
- const displayDepTree = (format: Format) => {
- if (format.header) {
- console.log(format.header);
- }
- return (filter: Filter) => (action: Audit) => (tree: PackageLockTree) => {
- const iter = (tree: PackageLockTree, stack: string[] = []) => {
- if (filter(tree, stack)) {
- action(tree).then(auditResult => console.log(format.apply(tree, stack, auditResult)));
- }
- if (tree.type === "node") {
- tree.children.forEach(t => iter(t, [...stack, `${tree.value.name}@${tree.value.version}`]))
- }
- }
- iter(tree);
- };
- }
- type AuditResponse = {
- ltID: string,
- baID: string;
- Version: string;
- status: string;
- initialUseCase: string;
- ltExpiryStatus: string;
- ltExpiryDate: string | Date;
- baExpiryStatus: string;
- baExpiryDate: string | Date;
- };
- type AuditResult = {
- status: "registered" | "expired" | "not registered" | "n/a" | "not found";
- expiryDate?: string;
- registeredVersion?: string;
- registeredExpiryDate?: string;
- lastExpiredVersion?: string;
- lastExpiredExpiryDate?: string;
- }
- const auditTreeNode: Audit = node => fetch(`https://oscs.us.oracle.com/oscs/pls/isplsapproved?software=${node.value.name}`)
- .then<AuditResponse[]>(res => res.json())
- .then<AuditResult>(auditResp => {
- if (auditResp.length === 0) {
- return {
- status: "not found"
- };
- }
- const predicateRegistered = (a: AuditResponse) => a.baExpiryStatus === "Not expired" && a.ltExpiryStatus === "Not expired";
- const predicateExpired = (a: AuditResponse) => a.baExpiryStatus === "Expired" && a.ltExpiryStatus === "Expired";
- const sortedAuditResp = auditResp
- .map(a => ({ ...a, baExpiryDate: new Date(a.baExpiryDate) }))
- .sort((lhs, rhs) => rhs.baExpiryDate.valueOf() - lhs.baExpiryDate.valueOf());
- const exactMatches = sortedAuditResp.filter(a => a.Version === node.value.version);
- const registeredExactMathces = exactMatches.filter(predicateRegistered);
- const expiredExactMatches = exactMatches.filter(predicateExpired);
- const registeredMatches = sortedAuditResp.filter(predicateRegistered);
- const expiredMatches = sortedAuditResp.filter(predicateExpired);
- return {
- status: registeredExactMathces.length !== 0
- ? "registered"
- : (expiredExactMatches.length !== 0 ? "expired" : "not registered"),
- expiryDate: registeredExactMathces.length !== 0
- ? formatDate(registeredExactMathces[0].baExpiryDate)
- : (expiredExactMatches.length !== 0 ? formatDate(expiredExactMatches[0]?.baExpiryDate) : formatDate(exactMatches[0]?.baExpiryDate)),
- registeredVersion: registeredMatches[0]?.Version,
- registeredExpiryDate: formatDate(registeredMatches[0]?.baExpiryDate),
- lastExpiredVersion: expiredMatches[0]?.Version,
- lastExpiredExpiryDate: formatDate(expiredMatches[0]?.baExpiryDate)
- };
- })
- .catch(_ => ({ status: "n/a" }));
- const formatDate = (date: Date): string =>
- date
- ? `${date.getUTCFullYear()}-${(date.getUTCMonth() + 1).toString().padStart(2, "0")}-${date.getUTCDate().toString().padStart(2, "0")}`
- : "";
- Promise.all<Package, PackageLock>([
- import(args.package),
- import(`${path.dirname(args.package)}/package-lock.json`)
- ])
- .then(([json, jsonLock]) => {
- const dependencyTree = Object.keys(json.dependencies)
- .concat(Object.keys(json.devDependencies ?? {}))
- .map(depName => constructDepTree(depName, jsonLock.dependencies));
- const display = displayDepTree(args.format)(args.filter)(args.audit);
- dependencyTree.forEach(t => display(t));
- })
- .catch(reson => {
- console.error(reson);
- });
|