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