dependency-tree.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import yargs from "yargs/yargs";
  2. import path from "path";
  3. import fetch from "node-fetch";
  4. import { hideBin } from "yargs/helpers";
  5. type FormatValue = "tree" | "csv";
  6. type Format = { header: string, apply: (node: PackageLockTree, parents: string[], audit: AuditResult) => string }
  7. type FilterValue = "none" | "oracle";
  8. type Filter = (node: PackageLockTree, parents: string[]) => boolean;
  9. type Audit = (node: PackageLockTree) => Promise<AuditResult>;
  10. const args = yargs(hideBin(process.argv))
  11. .option("package", {
  12. type: "string",
  13. describe: "The full or relative path to the package.json file",
  14. require: true
  15. })
  16. .option("format", {
  17. type: "string",
  18. describe: "Possible values are 'tree' and 'csv'",
  19. default: "csv",
  20. coerce: (value: FormatValue): Format =>
  21. value === "tree"
  22. ? {
  23. header: "",
  24. apply: (node, parents, audit) =>
  25. `${" ".repeat(parents.length)}${node.value.name}@${node.value.version}${node.value.dev ? " (dev)" : ""} audit:${audit}`
  26. }
  27. : {
  28. header: "name,version,root,parent,type,audit",
  29. apply: (node, parents, audit) =>
  30. `${node.value.name},${node.value.version},${parents[0] ?? ""},${parents[parents.length - 1] ?? ""},${node.value.dev ? "devDependency" : "dependency"},${audit}`
  31. }
  32. })
  33. .option("filter", {
  34. type: "string",
  35. describe: "Possible values are 'none' and 'oracle'",
  36. default: "oracle",
  37. coerce: (value: FilterValue): Filter =>
  38. value === "oracle"
  39. ? (node, parents) => (parents.length === 0 && !node.value.name.startsWith("@os")) || (!node.value.name.startsWith("@o") && parents.length > 1 && parents.every(p => p.startsWith("@os")))
  40. : (_, __) => true
  41. })
  42. .option("audit", {
  43. type: "boolean",
  44. describe: "Open Source Compliance Service audit",
  45. default: false,
  46. coerce: (value: boolean): Audit =>
  47. value
  48. ? node => auditTreeNode(node)
  49. : _ => Promise.resolve(AuditResult.NA)
  50. })
  51. .help()
  52. .argv;
  53. interface Dictionary<T> {
  54. [index: string]: T
  55. }
  56. interface Package {
  57. dependencies: Dictionary<string>;
  58. devDependencies?: Dictionary<string>;
  59. }
  60. type PackageLockDependencies = Dictionary<{
  61. version: string;
  62. dev: boolean;
  63. requires?: Dictionary<string>;
  64. dependencies?: PackageLockDependencies;
  65. }>
  66. interface PackageLock {
  67. dependencies: PackageLockDependencies;
  68. }
  69. type Tree<T> = Leaf<T> | Node<T>;
  70. type Node<T> = {
  71. type: "node";
  72. value: T;
  73. children: Tree<T>[];
  74. }
  75. type Leaf<T> = {
  76. type: "leaf";
  77. value: T;
  78. }
  79. type PackageLockTree = Tree<{ name: string, version: string, dev: boolean }>;
  80. const constructDepTree = (packageName: string, context: PackageLockDependencies, stack: string[] = []): PackageLockTree => {
  81. const dependency = context[packageName];
  82. const value = { name: packageName, version: dependency.version, dev: dependency.dev };
  83. if (!dependency.requires) {
  84. return { type: "leaf", value };
  85. }
  86. return {
  87. type: "node",
  88. value,
  89. children: Object.keys(dependency.requires).filter(k => stack.every(s => s !== k))
  90. .map(requiredDepName => constructDepTree(requiredDepName, { ...context, ...dependency.dependencies }, [...stack, packageName]))
  91. };
  92. }
  93. const displayDepTree = (format: Format) => {
  94. if (format.header) {
  95. console.log(format.header);
  96. }
  97. return (filter: Filter) => (action: Audit) => (tree: PackageLockTree) => {
  98. const iter = (tree: PackageLockTree, stack: string[] = []) => {
  99. if (filter(tree, stack)) {
  100. action(tree).then(auditResult => console.log(format.apply(tree, stack, auditResult)));
  101. }
  102. if (tree.type === "node") {
  103. tree.children.forEach(t => iter(t, [...stack, `${tree.value.name}@${tree.value.version}`]))
  104. }
  105. }
  106. iter(tree);
  107. };
  108. }
  109. type AuditResponse = {
  110. ltID: string,
  111. baID: string;
  112. status: string;
  113. initialUseCase: string;
  114. ltExpiryStatus: string;
  115. baExpiryStatus: string;
  116. }[];
  117. const enum AuditResult {
  118. OK = "registered",
  119. NOK = "not registered",
  120. NA = "n/a"
  121. }
  122. const auditTreeNode: Audit = node => fetch(`https://oscs.us.oracle.com/oscs/pls/isplsapproved?software=${node.value.name}&version=${node.value.version}`)
  123. .then<AuditResponse>(res => res.json())
  124. .then(auditResp =>
  125. auditResp.some(_ => true) && auditResp.every(a => a.baExpiryStatus !== "Expired" && a.ltExpiryStatus !== "Expired")
  126. ? AuditResult.OK
  127. : AuditResult.NOK)
  128. .catch(_ => AuditResult.NA)
  129. Promise.all<Package, PackageLock>([
  130. import(args.package),
  131. import(`${path.resolve(path.dirname(args.package))}/package-lock.json`)
  132. ])
  133. .then(([json, jsonLock]) => {
  134. const dependencyTree = Object.keys(json.dependencies)
  135. .concat(Object.keys(json.devDependencies ?? {}))
  136. .map(depName => constructDepTree(depName, jsonLock.dependencies));
  137. const display = displayDepTree(args.format)(args.filter)(args.audit);
  138. dependencyTree.forEach(t => display(t));
  139. })
  140. .catch(reson => {
  141. console.error(reson);
  142. });