OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / check-public-artifact-secrets.mjs

188 lines · javascript ref:
#!/usr/bin/env node

import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const here = path.dirname(fileURLToPath(import.meta.url))
const repoRoot = path.resolve(here, '..')

const DEFAULT_TARGETS = [
  'README.md',
  'docs',
  '.github',
  'truenas-app',
  'unraid-app',
  'zimaos-app',
  'runtipi-app',
  'hexos-app'
]

const SKIP_DIRS = new Set([
  '.git',
  'node_modules'
])

const SKIP_EXTENSIONS = new Set([
  '.gif',
  '.jpg',
  '.jpeg',
  '.pdf',
  '.png',
  '.svg',
  '.webp'
])

const PATTERNS = [
  {
    name: 'GitHub token prefix example',
    regex: /(?:gh[pousr]_|github_pat_)/
  },
  {
    name: 'private key delimiter example',
    regex: /-----BEGIN (?:OPENSSH |RSA |EC |)PRIVATE KEY-----/
  },
  {
    name: 'Bearer authorization example',
    regex: /Authorization:\s*Bearer\s+[A-Za-z0-9_-]{20,}/
  }
]

const usage = `
Usage:
  node scripts/check-public-artifact-secrets.mjs [--root <path>] [--json]

Scans public documentation and GitHub workflow files for scanner-sensitive
secret examples. Test fixtures and implementation regexes are intentionally
outside this audit; this command protects files that are likely to be read,
copied, or scanned as public release artifacts.
`

if (isMain()) main()

export function scanPublicArtifacts (opts = {}) {
  const root = path.resolve(opts.root || repoRoot)
  const targets = opts.targets || DEFAULT_TARGETS
  const files = []

  for (const target of targets) {
    const abs = path.resolve(root, target)
    if (!exists(abs)) continue
    collectFiles(abs, files)
  }

  const findings = []
  for (const file of files) {
    const rel = slash(path.relative(root, file))
    const text = fs.readFileSync(file, 'utf8')
    for (const pattern of PATTERNS) {
      const lines = findPatternLines(text, pattern.regex)
      for (const line of lines) {
        findings.push({
          path: rel,
          line,
          pattern: pattern.name
        })
      }
    }
  }

  return {
    ok: findings.length === 0,
    filesScanned: files.length,
    findings
  }
}

function main () {
  const args = parseArgs(process.argv.slice(2))
  const result = scanPublicArtifacts({ root: args.root })

  if (args.json) {
    console.log(JSON.stringify(result, null, 2))
  } else if (result.ok) {
    console.log(`Public artifact secret-pattern audit passed (${result.filesScanned} files scanned).`)
  } else {
    console.error('Public artifact secret-pattern audit failed:')
    for (const finding of result.findings) {
      console.error(`- ${finding.path}:${finding.line} ${finding.pattern}`)
    }
  }

  process.exit(result.ok ? 0 : 1)
}

function collectFiles (entry, out) {
  const stat = fs.statSync(entry)
  if (stat.isDirectory()) {
    const name = path.basename(entry)
    if (SKIP_DIRS.has(name)) return
    for (const child of fs.readdirSync(entry).sort()) {
      collectFiles(path.join(entry, child), out)
    }
    return
  }

  if (!stat.isFile()) return
  if (SKIP_EXTENSIONS.has(path.extname(entry).toLowerCase())) return
  out.push(entry)
}

function findPatternLines (text, regex) {
  const lines = String(text || '').split(/\r?\n/)
  const hits = []
  for (let i = 0; i < lines.length; i++) {
    if (regex.test(lines[i])) hits.push(i + 1)
  }
  return hits
}

function parseArgs (argv) {
  const out = {}
  for (let i = 0; i < argv.length; i++) {
    const arg = argv[i]
    if (arg === '--help' || arg === '-h') {
      console.log(usage.trim())
      process.exit(0)
    }
    if (arg === '--root') {
      out.root = readValue(argv, ++i, arg)
      continue
    }
    if (arg === '--json') {
      out.json = true
      continue
    }
    die(`Unknown argument: ${arg}`)
  }
  return out
}

function readValue (argv, index, flag) {
  const value = argv[index]
  if (!value || value.startsWith('--')) die(`Missing value for ${flag}`)
  return value
}

function exists (file) {
  try {
    fs.accessSync(file)
    return true
  } catch (_) {
    return false
  }
}

function slash (value) {
  return String(value).split(path.sep).join('/')
}

function isMain () {
  return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
}

function die (message) {
  console.error(message)
  process.exit(1)
}