PHP WebShell

Текущая директория: /opt/BitGoJS/node_modules/improved-yarn-audit/bin

Просмотр файла: improved-yarn-audit

#! /usr/bin/env node
const { execSync, spawn } = require("child_process")
const { randomBytes } = require("crypto")
const {
  existsSync,
  createReadStream,
  createWriteStream,
  mkdtempSync,
  readFileSync,
  rmdirSync,
  statSync,
  writeFileSync,
  unlinkSync
} = require("fs")
const { tmpdir } = require("os")
const path = require("path")
const { env, exit, platform } = require("process")
const { createInterface } = require("readline")

const GITHUB_ADVISORY_CODE = "GHSA"

const joinPath = path.join
const isWindows = platform === "win32"

const packageInfo = require(joinPath(__dirname, "..", "package.json"))

const optionFlags = {
  outputFormat: ["--output-format", "-of"],
  outputPath: ["--output-path", "-op"],
  severity: ["--min-severity", "-s"],
  exclude: ["--exclude", "-e"],
  retryNetworkIssues: ["--retry-on-network-failure", "-r"],
  ignoreDevDependencies: ["--ignore-dev-deps", "-i"],
  failOnMissingExclusions: ["--fail-on-missing-exclusions", "-f"],
  quiet: ["--quiet", "-q"],
  debug: ["--debug", "-d"],
  version: ["--version", "-v"],
  help: ["--help", "-h"]
}

const severityToIntMap = {
  info: 0,
  low: 1,
  moderate: 2,
  high: 3,
  critical: 4
}

const outputFormats = [
  "text",
  "json",
  "yarn-json"
]

const maxSeverityNameLength = 8

const exclusionsFileName = ".iyarc"

let logEnabled = true
let outputFormat = "text"
let outputPath = null
let minSeverityName = "low"
let minSeverity = severityToIntMap[minSeverityName]
let excludedAdvisories = []
let ignoreDevDependencies = false
let failOnMissingAdvisoryExclusions = false
let debugEnabled = false
let shouldRetryNetworkErrors = false
let auditResultsFilePath = "/dev/null"

function severityShouldBeIgnored(severity) {
  return severityToIntMap[severity] < minSeverity
}

function flatMap(arr, callback) {
  let arrays = arr
  let returnArray = []

  if (typeof callback === "function") {
    arrays = arr.map(callback)
  }

  arrays.forEach((a) => a.forEach((i) => returnArray.push(i)))

  return returnArray
}

async function logDebug(strOrFunc) {
  if (!debugEnabled || !logEnabled) {
    return
  }

  let output = typeof strOrFunc === "function" ? await strOrFunc() : strOrFunc

  console.log(`DEBUG: ${output}`)
}

function log(str) {
  if (logEnabled) {
    console.log(str)
  }
}

function checkForMissingExclusions(allAdvisories) {
  const missingExcludedAdvisories = excludedAdvisories.filter(
    (ea) =>
      allAdvisories.find((a) => a.id === ea || a.github_advisory_id === ea) ===
      undefined
  )

  if (missingExcludedAdvisories.length < 1) {
    logDebug("No missing advisory exclusions found")

    return
  }

  log()

  log(
    "WARNING: One or more excluded audit advisories were missing from yarn audit output: " +
      `${missingExcludedAdvisories.join(",")}`
  )

  if (failOnMissingAdvisoryExclusions) {
    console.error(
      `ERROR: ${optionFlags.failOnMissingExclusions.join(
        "/"
      )} was specified, ` +
        "exit code will indicate number of missing exclusions"
    )
    process.exit(missingExcludedAdvisories.length)
  }
}

function reportIgnoredAdvisories(
  devDependencyAdvisoryIds,
  severityIgnoredAuditAdvisories,
  excludedAuditAdvisories
) {
  if (ignoreDevDependencies && devDependencyAdvisoryIds.length > 0) {
    console.warn(`${devDependencyAdvisoryIds.length} ignored because ` +
      `they are dev dependencies\n`)
  }

  if (severityIgnoredAuditAdvisories.length > 0) {
    console.warn(`${severityIgnoredAuditAdvisories.length} ignored because ` +
      `severity was lower than "${minSeverityName}"\n`)
  }

  if (excludedAuditAdvisories.length > 0) {
    console.warn(`${excludedAuditAdvisories.length} ignored because of advisory exclusions\n`)
  }
}

async function createReport(
  filteredAuditAdvisories,
  devDependencyAdvisories,
  devDependencyAdvisoryIds,
  severityIgnoredAuditAdvisories,
  excludedAuditAdvisories
) {
  logDebug(() => `Dev audit advisories:\n${toJson(devDependencyAdvisories)}\n`)
  logDebug(
    () => `Excluded audit advisories:\n${toJson(excludedAuditAdvisories)}\n`
  )
  logDebug(
    () =>
      `Severity ignored advisories:\n${toJson(
        severityIgnoredAuditAdvisories
      )}\n`
  )

  log(`Found ${filteredAuditAdvisories.length} vulnerabilities\n`)
  reportIgnoredAdvisories(
    devDependencyAdvisoryIds,
    severityIgnoredAuditAdvisories,
    excludedAuditAdvisories
  )

  const outputText = outputFormat === "text" 
    ? createTextReport(filteredAuditAdvisories)
    : await createJsonReport(filteredAuditAdvisories, outputFormat === "yarn-json")

  if (isNullOrEmpty(outputPath)) {
    console.log(`${outputText}`)
  } else {
    writeFileSync(outputPath, outputText)
  }

  return filteredAuditAdvisories.length
}

const createTextReport = filteredAuditAdvisories =>
  filteredAuditAdvisories.map((a) => {
    const formattedSeverity = a.severity
      .toUpperCase()
      .padEnd(maxSeverityNameLength, " ")

    const affectedModulePaths = flatMap(a.findings, (f) => f.paths)
    const affectedModules = affectedModulePaths.join(", ")

    return `Vulnerability Found:

  Severity: ${formattedSeverity}
  Modules: ${affectedModules}
  URL: ${a.url}`
  }).join("\n\n")

async function createJsonReport(filteredAuditAdvisories, produceYarnJson) {
  const report = filteredAuditAdvisories.map(a =>  {
    return {
      type: "auditAdvisory",
      data: {
        resolution: {
          id: a.id,
          // TODO: handle different paths for the same vulnerability
          path: a.findings[0].paths[0], 
          dev: a._resolution.dev,
          optional: a._resolution.optional,
          bundled: a._resolution.bundled
        },
        advisory: a
      }
    }
  })

  const getAdvisoryCountBySeverity = s => filteredAuditAdvisories.filter(a => a.severity === s).length
  const auditSummary = await getAuditSummary()

  auditSummary.data.vulnerabilities = {
    info: getAdvisoryCountBySeverity("info"),
    low: getAdvisoryCountBySeverity("low"),
    moderate: getAdvisoryCountBySeverity("moderate"),
    high: getAdvisoryCountBySeverity("high"),
    critical: getAdvisoryCountBySeverity("critical")
  }

  report.push(auditSummary)

  return produceYarnJson 
    ? report.map(e => JSON.stringify(e)).join("\n") 
    : json = JSON.stringify(report)
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms))

function isNonExcludedAdvisory(advisory, devDependencyAdvisoryIds) {
  return (
    !severityShouldBeIgnored(advisory.severity) &&
    !excludedAdvisories.includes(advisory.id) &&
    !excludedAdvisories.includes(advisory.github_advisory_id) &&
    (!devDependencyAdvisoryIds.includes(advisory.id) || !ignoreDevDependencies)
  )
}

function parseAuditJson(jsonString) {
  try {
    return JSON.parse(jsonString)
  } catch (ex) {
    console.error(`ERROR: Unable to parse yarn audit output: ${ex}`)
    console.error("Try running `yarn audit` for more info")

    process.exit(1)
  }
}

async function getAuditSummary() {
  logDebug("Getting yarn audit summary")

  let auditSummary = ""

  await iterateOverAuditResults((l) => (auditSummary = l))

  return JSON.parse(auditSummary)
}

async function handleAuditNetworkError(output) {
  const error = "ERROR: Network error occurred when querying audit registry"

  if (!shouldRetryNetworkErrors) {
    errorAndExit(`${error}\n\n${output}`)
  }

  console.error(`${error}, retrying...\n`)

  await sleep(1000)

  return await runYarnAudit()
}

async function dumpAuditResultsAsString() {
  let output = ""

  await iterateOverAuditResults((l) => (output += l))

  return output
}

async function iterateOverAuditResults(action) {
  logDebug("Iterating over audit results")

  const auditResultsFileStream = getAuditResultsFileStream("r")
  const iterator = createInterface(auditResultsFileStream)

  iterator.on("line", action)

  await new Promise((resolve) => iterator.on("close", resolve))

  auditResultsFileStream.close()
}

function getAuditResultsFileStream(mode) {
  logDebug(
    `Opening file stream for file '${auditResultsFilePath}' in '${mode}' mode`
  )

  if (mode === "w") {
    return createWriteStream(auditResultsFilePath)
  }

  return createReadStream(auditResultsFilePath)
}

async function cleanupAuditResultsFile() {
  if (!existsSync(auditResultsFilePath)) {
    return
  }

  if (isWindows) {
    // workaround for unlinkSync issues on windows
    execSync(`del "${auditResultsFilePath}"`)

    return
  }

  unlinkSync(auditResultsFilePath)
}

async function streamYarnAuditOutput(auditParams, auditResultsFileStream) {
  const yarnBinaryPostFix = isWindows ? ".cmd" : ""
  const yarnProcess = spawn(`yarn${yarnBinaryPostFix}`, auditParams, {
    env: env,
    shell: isWindows,
    stdio: ["pipe", auditResultsFileStream, auditResultsFileStream]
  })

  let exitCode = await new Promise((resolve, reject) =>
    yarnProcess.on("exit", resolve).on("error", reject)
  )

  auditResultsFileStream.close()

  logDebug(
    () => `Yarn audit output size: ${statSync(auditResultsFilePath).size} bytes`
  )

  return exitCode
}

async function invokeYarnAudit() {
  const auditParams = ["audit", "--json", `--level=${minSeverityName}`]

  if (ignoreDevDependencies) {
    auditParams.push("--groups=dependencies")
  }

  cleanupAuditResultsFile()

  const auditResultsFileStream = getAuditResultsFileStream("w")

  let exitCode = await new Promise((resolve) =>
    auditResultsFileStream.on("open", async () => {
      let exitCode = await streamYarnAuditOutput(
        auditParams,
        auditResultsFileStream
      )

      resolve(exitCode)
    })
  )

  return exitCode
}

async function runYarnAudit() {
  log("Running yarn audit...\n")

  const exitCode = await invokeYarnAudit()

  let networkErrorHasOccurred = false

  if (existsSync(auditResultsFilePath)) {
    await iterateOverAuditResults(
      (a) =>
        (networkErrorHasOccurred =
          networkErrorHasOccurred || a.includes("Error: Request failed "))
    )
  }

  logDebug(`networkErrorHasOccurred: ${networkErrorHasOccurred}`)

  if (networkErrorHasOccurred) {
    return await handleAuditNetworkError()
  }

  if (exitCode === 1) {
    errorAndExit(
      `ERROR: Yarn audit error:\n${await dumpAuditResultsAsString()}`
    )
  }
}

function getDevDependenciesRegex() {
  if (!existsSync("package.json")) {
    log(
      "WARNING: No package.json was found in the current working directory"
    )
    logDebug("Dev dependencies will not be ignored")

    return
  }

  const packageJson = readFileSync("package.json")
  const package = JSON.parse(packageJson)

  let devDependencies = []

  if (
    typeof package === "object" &&
    typeof package.devDependencies === "object"
  ) {
    devDependencies = Object.keys(package.devDependencies)
  }

  if (devDependencies.length < 1) {
    logDebug("No dev dependencies installed")
    logDebug("Dev dependencies will not be ignored")

    return
  }

  logDebug(() => `Dev dependencies: ${devDependencies.join(", ")}`)

  ignoreDevDependencies
    ? logDebug("Dev dependencies will be ignored")
    : logDebug("Dev dependencies will not be ignored")

  const devDependenciesOr = devDependencies.map((d) => `(${d})`).join("|")
  const devDependenciesRegex = `^${devDependenciesOr}>*.*$`

  logDebug(`Dev dependencies regex: ${devDependenciesRegex}`)

  return new RegExp(devDependenciesRegex)
}

async function runAuditReport() {
  const devDependenciesRegex = getDevDependenciesRegex()

  await runYarnAudit()

  logDebug(async () => {
    let auditSummaryJson = await getAuditSummary()
    let auditSummary = toJson(auditSummaryJson)

    return `Audit summary:\n${auditSummary}\n`
  })

  const allAdvisories = []
  const filteredAuditAdvisories = []
  const severityIgnoredAuditAdvisories = []
  const excludedAuditAdvisories = []

  let devDependencyAdvisories = []
  let devDependencyAdvisoryIds = []

  await iterateOverAuditResults((resultJson) => {
    const potentialResult = parseAuditJson(resultJson)

    if (
      typeof potentialResult.type !== "string" ||
      potentialResult.type !== "auditAdvisory"
    ) {
      return
    }

    const result = potentialResult.data.advisory
    result._resolution = potentialResult.data.resolution

    allAdvisories.push(result)

    if (devDependenciesRegex) {
      const isDevDependencyAdvisory = flatMap(
        result.findings,
        (f) => f.paths
      ).every((d) => d.match(devDependenciesRegex))

      if (isDevDependencyAdvisory) {
        devDependencyAdvisories = devDependencyAdvisories.concat(result)
        devDependencyAdvisoryIds = devDependencyAdvisories.concat(
          devDependencyAdvisories.map((d) => d.id)
        )
      }
    }

    if (isNonExcludedAdvisory(result, devDependencyAdvisoryIds)) {
      filteredAuditAdvisories.push(result)
    }

    if (
      (excludedAdvisories.includes(result.id) ||
        excludedAdvisories.includes(result.github_advisory_id)) &&
      !severityShouldBeIgnored(result.severity)
    ) {
      excludedAuditAdvisories.push(result)
    }

    if (severityShouldBeIgnored(result.severity)) {
      severityIgnoredAuditAdvisories.push(result)
    }
  })

  checkForMissingExclusions(allAdvisories)

  return await createReport(
    filteredAuditAdvisories,
    devDependencyAdvisories,
    devDependencyAdvisoryIds,
    severityIgnoredAuditAdvisories,
    excludedAuditAdvisories
  ) 
}

async function withTempDir(action, cleanupAction) {
  const prefix = joinPath(tmpdir(), "iya")
  const tempDirPath = mkdtempSync(prefix)

  try {
    return await action(tempDirPath)
  } finally {
    await cleanupAction()

    rmdirSync(tempDirPath, { recursive: true })
  }
}

async function withTempFile(action, cleanupAction) {
  return await withTempDir((tempDirPath) => {
    const randomName = randomBytes(16).toString("hex")
    const tmpFilePath = joinPath(tempDirPath, randomName)

    return action(tmpFilePath)
  }, cleanupAction)
}

function toJson(value) {
  return JSON.stringify(value, null, 2)
}

function printVersionAndExit() {
  errorAndExit(packageInfo.version)
}

function printUsageAndExit() {
  errorAndExit(`
improved-yarn-audit [OPTIONS]

Options:
  --min-severity, -s                  Minimum severity to treat as an error, default is low (info, low, moderate, high, critical)
  --exclude, -e                       CSV list of advisory ID's to ignore, e.x. 432,564 (this overrides .iyarc)
  --retry-on-network-failure, -r      Retry audit if NPM registry throws a network error
  --ignore-dev-deps, -i               Ignore advisories for dev dependencies
  --fail-on-missing-exclusions, -f    Return a non-zero exit code when advisory exclusions are no longer detected by yarn audit
  --output-path, -op                  The path to the output file to write audit reports to - if not provided stdout is used
  --output-format, -of                The format of the audit report to produce (text, json or yarn-json) - default is text
  --quiet, -q                         Don't print out any log lines to stdout, only audit reports
  --debug, -d                         Print out raw audit report's and advisory details
  --version, -v                       Print version info and exit
  --help, -h                          Show this information

The CSV list of advisory ID's can also be provided in a '.iyarc' file in the current working directory. This file also supports
comments, lines beginning with a '#' character are ignored.
`)
}

function errorAndExit(msg) {
  console.error(msg)
  process.exit(1)
}

function parseJSONSafeExcludeList(str) {
  return str
    .split(",")
    .map((option) => {
      if (option.startsWith(GITHUB_ADVISORY_CODE)) {
        return `"${option}"`
      }

      return option
    })
    .filter((str) => str)
    .join(",")
}

function parseConfigToJson(str) {
  const jsonSafeString = parseJSONSafeExcludeList(str)

  return JSON.parse(`[${jsonSafeString}]`)
}

function isValidConfigFormat(str) {
  try {
    const testExcludeList = parseConfigToJson(str)

    testExcludeList.forEach((excludeOption) => {
      if (
        parseInt(excludeOption) === NaN &&
        !excludeOption.startsWith(GITHUB_ADVISORY_CODE)
      ) {
        throw "Invalid format"
      }
    })

    return true
  } catch {
    return false
  }
}

function isNullOrEmpty(str) {
  return typeof str !== "string" || str.trim() === ""
}

function loadExclusionsFromFileIfPresent() {
  if (!existsSync(exclusionsFileName)) {
    logDebug(`No ${exclusionsFileName} found in working directory`)
    return
  }

  if (excludedAdvisories.length > 0) {
    log(
      `WARNING: Ignoring ${exclusionsFileName} as exclusions were passed in via command line`
    )
    return
  }

  let matchedNpmAdvisories = readFileSync(exclusionsFileName)
    .toString()
    .match(/(?<=^(?:\d+,)*)\d+(?=(?:,\d+)*$)/gm)

  let matchedGithubAdvisories = readFileSync(exclusionsFileName)
    .toString()
    .match(/^(GHSA-([a-z0-9]{4})-([a-z0-9]{4})-([a-z0-9]{4}))$/gm)

  let advisoriesNPMCsv = matchedNpmAdvisories
    ? matchedNpmAdvisories.join(",")
    : ""

  let advisoriesGithubCsv = matchedGithubAdvisories
    ? matchedGithubAdvisories.join(",")
    : ""

  let advisoriesCsv = [advisoriesNPMCsv, advisoriesGithubCsv].join(",")

  logDebug(`.iyarc contents (excluding comments): ${advisoriesCsv}`)

  if (!isValidConfigFormat(advisoriesCsv)) {
    errorAndExit(
      `ERROR: ${exclusionsFileName} is not in the correct format, excluded advisories must be provided on ` +
        "individual lines, or as a CSV list (eg: '2341,21,43,GHSA-42xw-2xvc-qx8mas')"
    )
  }

  log(`Reading excluded advisories from ${exclusionsFileName}`)

  excludedAdvisories = parseConfigToJson(advisoriesCsv)
}

function isFlag(flags, ...strings) {
  return (
    strings.filter(
      (s) => !isNullOrEmpty(s) && flags.includes(s.trim().toLowerCase())
    ).length > 0
  )
}

function parseCommandLineArgs() {
  process.argv.reduce((a, b) => {
    if (isFlag(optionFlags.version, a, b)) {
      printVersionAndExit()
    }

    if (isFlag(optionFlags.help, a, b)) {
      printUsageAndExit()
    }

    if (isFlag(optionFlags.exclude, a) && !isNullOrEmpty(b)) {
      if (isValidConfigFormat(b)) {
        excludedAdvisories = parseConfigToJson(b)
      } else {
        errorAndExit(`ERROR: Unable to parse --exclude option value: ${b}`)
      }
    }

    if (isFlag(optionFlags.severity, a) && !isNullOrEmpty(b)) {
      minSeverityName = b.trim().toLowerCase()
      minSeverity = severityToIntMap[minSeverityName]

      if ([null, undefined].includes(minSeverity)) {
        errorAndExit(
          `ERROR: Unrecognised --min-severity option value: ${minSeverityName}`
        )
      }
    }

    if (isFlag(optionFlags.outputFormat, a) && !isNullOrEmpty(b)) {
      outputFormat = b.trim().toLowerCase()

      if (outputFormats.indexOf(outputFormat) < 0) {
        errorAndExit(
          `ERROR: Unrecognised --output-format option value: ${outputFormat}`
        )
      }
    }

    if (isFlag(optionFlags.outputPath, a) && !isNullOrEmpty(b)) {
      outputPath = b.trim()
    }

    if (isFlag(optionFlags.quiet, a, b)) {
      logEnabled = false
    }

    if (isFlag(optionFlags.debug, a, b)) {
      debugEnabled = true
    }

    if (isFlag(optionFlags.retryNetworkIssues, a, b)) {
      shouldRetryNetworkErrors = true
    }

    if (isFlag(optionFlags.ignoreDevDependencies, a, b)) {
      ignoreDevDependencies = true
    }

    if (isFlag(optionFlags.failOnMissingExclusions, a, b)) {
      failOnMissingAdvisoryExclusions = true
    }

    return b
  }, "")
}

async function main() {
  parseCommandLineArgs()

  log(`Improved Yarn Audit - v${packageInfo.version}`)
  log()

  logDebug(`shouldRetryNetworkErrors = ${shouldRetryNetworkErrors}`)
  logDebug(`ignoreDevDependencies = ${ignoreDevDependencies}`)

  loadExclusionsFromFileIfPresent()

  log(`Minimum severity level to report: ${minSeverityName}`)

  log(
    excludedAdvisories.length > 0
      ? `Excluded Advisories: ${JSON.stringify(excludedAdvisories)}\n`
      : ""
  )

  try {
    let advisoryCount = await withTempFile((filePath) => {
      auditResultsFilePath = filePath

      logDebug(`Temporary file path: ${auditResultsFilePath}`)

      return runAuditReport()
    }, cleanupAuditResultsFile)

    if (advisoryCount > 0) {
      log("Run `yarn audit` for more information")
    }

    exit(advisoryCount)
  } catch (e) {
    console.error("Audit failed due to an error: \n")
    console.error(e)
  }

  exit(1)
}

main()

Выполнить команду


Для локальной разработки. Не используйте в интернете!