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; 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 { [index: string]: T } interface Package { dependencies: Dictionary; devDependencies?: Dictionary; } type PackageLockDependencies = Dictionary<{ version: string; dev: boolean; requires?: Dictionary; dependencies?: PackageLockDependencies; }> interface PackageLock { dependencies: PackageLockDependencies; } type Tree = Leaf | Node; type Node = { type: "node"; value: T; children: Tree[]; } type Leaf = { 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(res => res.json()) .then(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([ 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); });