You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

134 lines
3.6 KiB
JavaScript

#!/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`)}`);
});