#!/usr/bin/env node "use strict"; const Promise = require("bluebird"); const yargs = require("yargs"); const fs = require("fs"); const fsPromises = require("fs").promises; const path = require("path"); const isGlob = require("is-glob"); const globby = require("globby"); const splitFilterN = require("split-filter-n"); const chalk = require("chalk"); const matchValue = require("match-value"); const detectSVGScripts = require("detect-svg-scripts"); let argv = yargs .boolean("errors-only") .argv; let failureTypes = [ "externalScriptFile", "inlineScriptTag", "eventHandler" ]; Promise.map(argv._, (target) => { return Promise.try(() => { return fsPromises.stat(target); }).then((stats) => { if (stats.isDirectory()) { // Literal directory return { type: "glob", glob: path.posix.join(target, "**/*.svg") }; } else { // Literal single file return { type: "file", path: target }; } }).catch({ code: "ENOENT" }, (error) => { // Probably meant as a glob pattern if (isGlob(target)) { return { type: "glob", glob: target }; } else { // TODO: Make this output nicer throw error; } }); }).then((targets) => { if (targets.length === 0) { // Default to all SVGs in the current working directory, if the user hasn't specified any explicit paths to scan. targets = [{ type: "glob", glob: path.posix.join(process.cwd(), "**/*.svg") }]; } let targetsByType = splitFilterN(targets, [ "file", "glob" ], (target) => target.type); let literalFiles = targetsByType.file.map((target) => target.path); let globs = targetsByType.glob.map((target) => target.glob); return Promise.try(() => { return globby(globs); }).then((globbedPaths) => { return literalFiles.concat(globbedPaths); }); }).map((file) => { return Promise.try(() => { return detectSVGScripts(fs.createReadStream(file)); }).then((occurrences) => { if (occurrences.length > 0) { return { file: file, passed: false, occurrences: occurrences }; } else { return { file: file, passed: true, occurrences: occurrences }; } }); }).each((result) => { if (result.passed === true) { if (!argv.errorsOnly) { console.log(chalk.green(`${chalk.bold("[ ✔ PASSED ]")} ${result.file}`)); } } else { console.log(chalk.red(`${chalk.bold("[ ✘ FAILED ]")} ${result.file}`)); let failuresByType = splitFilterN(result.occurrences, failureTypes, (result) => result.type); let foundTypes = failureTypes .filter((type) => failuresByType[type].length > 0) .map((type) => { let failureCount = failuresByType[type].length; let suffix = matchValue(type, { externalScriptFile: "external script file(s)", inlineScriptTag: "inline script tag(s)", eventHandler: "inline event handler(s)" }); if (type === "eventHandler") { let attributes = failuresByType.eventHandler.map((item) => item.attribute); let uniqueAttributes = Array.from(new Set(attributes)); return `${failureCount} ${suffix}: ${uniqueAttributes.join(", ")}`; } else { return `${failureCount} ${suffix}`; } }); // Indent at the same depth as the filename console.log(chalk.gray(` └ Found ${foundTypes.join(", ")}`)); } }).then((allResults) => { let failedResults = allResults.filter((result) => result.passed !== true); let totalCount = allResults.length; let failedCount = failedResults.length; let passedCount = totalCount - failedCount; if (failedCount > 0) { process.exitCode = 1; } console.log(`Scanned ${totalCount} files, ${chalk.green(`${passedCount} passed`)}, ${chalk.red(`${failedCount} failed`)}`); });