dependency-tree.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  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. coerce: path.resolve
  16. })
  17. .option("format", {
  18. type: "string",
  19. describe: "Possible values are 'tree' and 'csv'",
  20. default: "csv",
  21. coerce: (value: FormatValue): Format =>
  22. value === "tree"
  23. ? {
  24. header: "",
  25. apply: (node, parents, audit) =>
  26. `${" ".repeat(parents.length)}${node.value.name}@${node.value.version}${node.value.dev ? " (dev)" : ""} audit:${audit.status}`
  27. }
  28. : {
  29. header: "name,version,root,parent,type,audit,expiry date,registered version,registered expiry date,last expired version,last expired expiry date",
  30. apply: (node, parents, audit) =>
  31. `${node.value.name},${node.value.version},${parents[0] ?? ""},${parents[parents.length - 1] ?? ""},${node.value.dev ? "devDependency" : "dependency"}` +
  32. `,${audit.status},${audit.expiryDate ?? ""},${audit.registeredVersion ?? ""},${audit.registeredExpiryDate ?? ""},${audit.lastExpiredVersion ?? ""},${audit.lastExpiredExpiryDate ?? ""}`
  33. }
  34. })
  35. .option("filter", {
  36. type: "string",
  37. describe: "Possible values are 'none' and 'oracle'",
  38. default: "oracle",
  39. coerce: (value: FilterValue): Filter =>
  40. value === "oracle"
  41. ? (node, parents) => (parents.length === 0 && !node.value.name.startsWith("@os")) || (!node.value.name.startsWith("@o") && parents.length > 0 && parents.every(p => p.startsWith("@os")))
  42. : (_, __) => true
  43. })
  44. .option("audit", {
  45. type: "boolean",
  46. describe: "Open Source Compliance Service audit",
  47. default: false,
  48. coerce: (value: boolean): Audit =>
  49. value
  50. ? node => auditTreeNode(node)
  51. : _ => Promise.resolve({ status: "n/a" })
  52. })
  53. .help()
  54. .argv;
  55. interface Dictionary<T> {
  56. [index: string]: T
  57. }
  58. interface Package {
  59. dependencies: Dictionary<string>;
  60. devDependencies?: Dictionary<string>;
  61. }
  62. type PackageLockDependencies = Dictionary<{
  63. version: string;
  64. dev: boolean;
  65. requires?: Dictionary<string>;
  66. dependencies?: PackageLockDependencies;
  67. }>
  68. interface PackageLock {
  69. dependencies: PackageLockDependencies;
  70. }
  71. type Tree<T> = Leaf<T> | Node<T>;
  72. type Node<T> = {
  73. type: "node";
  74. value: T;
  75. children: Tree<T>[];
  76. }
  77. type Leaf<T> = {
  78. type: "leaf";
  79. value: T;
  80. }
  81. type PackageLockTree = Tree<{ name: string, version: string, dev: boolean }>;
  82. const constructDepTree = (packageName: string, context: PackageLockDependencies, stack: string[] = []): PackageLockTree => {
  83. const dependency = context[packageName];
  84. const value = { name: packageName, version: dependency.version, dev: dependency.dev };
  85. if (!dependency.requires) {
  86. return { type: "leaf", value };
  87. }
  88. return {
  89. type: "node",
  90. value,
  91. children: Object.keys(dependency.requires).filter(k => stack.every(s => s !== k))
  92. .map(requiredDepName => constructDepTree(requiredDepName, { ...context, ...dependency.dependencies }, [...stack, packageName]))
  93. };
  94. }
  95. const displayDepTree = (format: Format) => {
  96. if (format.header) {
  97. console.log(format.header);
  98. }
  99. return (filter: Filter) => (action: Audit) => (tree: PackageLockTree) => {
  100. const iter = (tree: PackageLockTree, stack: string[] = []) => {
  101. if (filter(tree, stack)) {
  102. action(tree).then(auditResult => console.log(format.apply(tree, stack, auditResult)));
  103. }
  104. if (tree.type === "node") {
  105. tree.children.forEach(t => iter(t, [...stack, `${tree.value.name}@${tree.value.version}`]))
  106. }
  107. }
  108. iter(tree);
  109. };
  110. }
  111. type AuditResponse = {
  112. ltID: string,
  113. baID: string;
  114. Version: string;
  115. status: string;
  116. initialUseCase: string;
  117. ltExpiryStatus: string;
  118. ltExpiryDate: string | Date;
  119. baExpiryStatus: string;
  120. baExpiryDate: string | Date;
  121. };
  122. type AuditResult = {
  123. status: "registered" | "expired" | "not registered" | "n/a" | "not found";
  124. expiryDate?: string;
  125. registeredVersion?: string;
  126. registeredExpiryDate?: string;
  127. lastExpiredVersion?: string;
  128. lastExpiredExpiryDate?: string;
  129. }
  130. const auditTreeNode: Audit = node => fetch(`https://oscs.us.oracle.com/oscs/pls/isplsapproved?software=${node.value.name}`)
  131. .then<AuditResponse[]>(res => res.json())
  132. .then<AuditResult>(auditResp => {
  133. if (auditResp.length === 0) {
  134. return {
  135. status: "not found"
  136. };
  137. }
  138. const predicateRegistered = (a: AuditResponse) => a.baExpiryStatus === "Not expired" && a.ltExpiryStatus === "Not expired";
  139. const predicateExpired = (a: AuditResponse) => a.baExpiryStatus === "Expired" && a.ltExpiryStatus === "Expired";
  140. const sortedAuditResp = auditResp
  141. .map(a => ({ ...a, baExpiryDate: new Date(a.baExpiryDate) }))
  142. .sort((lhs, rhs) => rhs.baExpiryDate.valueOf() - lhs.baExpiryDate.valueOf());
  143. const exactMatches = sortedAuditResp.filter(a => a.Version === node.value.version);
  144. const registeredExactMathces = exactMatches.filter(predicateRegistered);
  145. const expiredExactMatches = exactMatches.filter(predicateExpired);
  146. const registeredMatches = sortedAuditResp.filter(predicateRegistered);
  147. const expiredMatches = sortedAuditResp.filter(predicateExpired);
  148. return {
  149. status: registeredExactMathces.length !== 0
  150. ? "registered"
  151. : (expiredExactMatches.length !== 0 ? "expired" : "not registered"),
  152. expiryDate: registeredExactMathces.length !== 0
  153. ? formatDate(registeredExactMathces[0].baExpiryDate)
  154. : (expiredExactMatches.length !== 0 ? formatDate(expiredExactMatches[0]?.baExpiryDate) : formatDate(exactMatches[0]?.baExpiryDate)),
  155. registeredVersion: registeredMatches[0]?.Version,
  156. registeredExpiryDate: formatDate(registeredMatches[0]?.baExpiryDate),
  157. lastExpiredVersion: expiredMatches[0]?.Version,
  158. lastExpiredExpiryDate: formatDate(expiredMatches[0]?.baExpiryDate)
  159. };
  160. })
  161. .catch(_ => ({ status: "n/a" }));
  162. const formatDate = (date: Date): string =>
  163. date
  164. ? `${date.getUTCFullYear()}-${(date.getUTCMonth() + 1).toString().padStart(2, "0")}-${date.getUTCDate().toString().padStart(2, "0")}`
  165. : "";
  166. Promise.all<Package, PackageLock>([
  167. import(args.package),
  168. import(`${path.dirname(args.package)}/package-lock.json`)
  169. ])
  170. .then(([json, jsonLock]) => {
  171. const dependencyTree = Object.keys(json.dependencies)
  172. .concat(Object.keys(json.devDependencies ?? {}))
  173. .map(depName => constructDepTree(depName, jsonLock.dependencies));
  174. const display = displayDepTree(args.format)(args.filter)(args.audit);
  175. dependencyTree.forEach(t => display(t));
  176. })
  177. .catch(reson => {
  178. console.error(reson);
  179. });