Initial commit
commit
93f8757032
@ -0,0 +1 @@
|
||||
node_modules
|
@ -0,0 +1,51 @@
|
||||
# scriptless-svg
|
||||
|
||||
A simple command-line tool for detecting SVG files that contain embedded scripts (eg. Javascript), which may be undesirable from a security perspective. Uses [detect-svg-scripts](https://www.npmjs.com/package/detect-svg-scripts) for scanning.
|
||||
|
||||
If you want to integrate SVG scanning into a bigger application, you should use [detect-svg-scripts](https://www.npmjs.com/package/detect-svg-scripts) directly instead. This package __only__ contains a CLI tool for it.
|
||||
|
||||
## License, donations, and other boilerplate
|
||||
|
||||
Licensed under either the [WTFPL](http://www.wtfpl.net/txt/copying/) or [CC0](https://creativecommons.org/publicdomain/zero/1.0/), at your choice. In practice, that means it's more or less public domain, and you can do whatever you want with it. Giving credit is *not* required, but still very much appreciated! I'd love to [hear from you](mailto:admin@cryto.net) if this module was useful to you.
|
||||
|
||||
Creating and maintaining open-source modules is a lot of work. A donation is also not required, but much appreciated! You can donate [here](http://cryto.net/~joepie91/donate.html).
|
||||
|
||||
## Screenshot
|
||||
|
||||
When running `scriptless-svg` on the [Web Platform Tests for SVG](https://github.com/web-platform-tests/wpt/tree/master/svg):
|
||||
|
||||
![Screenshot](https://git.cryto.net/joepie91/scriptless-svg/raw/master/screenshot.png)
|
||||
|
||||
## Usage
|
||||
|
||||
`scriptless-svg` takes any amount of paths and/or [globs](https://www.npmjs.com/package/globby#globbing-patterns) as its arguments. If an argument doesn't exist as an exact path, it is assumed to be a glob (and will fail if not). You can include negated globs to exclude certain patterns.
|
||||
|
||||
Additionally, you can pass the `--errors-only` flag to omit all files from the output that passed the check successfully. This is especially recommended for CI setups where you are only interested in the failures.
|
||||
|
||||
The process will return exit code 1 if any scanned files failed the check (ie. contain scripts), or exit code 0 if all files passed.
|
||||
|
||||
## Examples
|
||||
|
||||
Scan all `*.svg* files in the current directory and any subdirectories:
|
||||
|
||||
```sh
|
||||
scriptless-svg
|
||||
```
|
||||
|
||||
Scan all `*.svg* files in a given target directory and its subdirectories:
|
||||
|
||||
```sh
|
||||
scriptless-svg /path/to/directory
|
||||
```
|
||||
|
||||
Complex globs, with eg. exclusions (note that globs should be single-quoted to work correctly!):
|
||||
|
||||
```sh
|
||||
scriptless-svg svg/ '!svg/scriptable/**/*.scriptable.svg'
|
||||
```
|
||||
|
||||
Show only the files that failed the check (ie. contain scripts), not the ones that passed:
|
||||
|
||||
```sh
|
||||
scriptless-svg --errors-only svg/
|
||||
```
|
@ -0,0 +1,133 @@
|
||||
#!/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`)}`);
|
||||
});
|
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "scriptless-svg",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"repository": "http://git.cryto.net/joepie91/scriptless-svg.git",
|
||||
"author": "Sven Slootweg <admin@cryto.net>",
|
||||
"license": "WTFPL OR CC0-1.0",
|
||||
"bin": {
|
||||
"scriptless-svg": "./bin/scriptless-svg"
|
||||
},
|
||||
"dependencies": {
|
||||
"bluebird": "^3.7.2",
|
||||
"chalk": "^4.1.0",
|
||||
"detect-svg-scripts": "^1.0.0",
|
||||
"globby": "^11.0.1",
|
||||
"is-glob": "^4.0.1",
|
||||
"match-value": "^1.1.0",
|
||||
"split-filter-n": "^1.1.2",
|
||||
"yargs": "^15.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joepie91/eslint-config": "^1.1.0",
|
||||
"eslint": "^7.6.0"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 112 KiB |
Loading…
Reference in New Issue