Browse Source

Initial commit

master
Sven Slootweg 7 months ago
commit
93f8757032
7 changed files with 1493 additions and 0 deletions
  1. +3
    -0
      .eslintrc
  2. +1
    -0
      .gitignore
  3. +51
    -0
      README.md
  4. +133
    -0
      bin/scriptless-svg
  5. +25
    -0
      package.json
  6. BIN
      screenshot.png
  7. +1280
    -0
      yarn.lock

+ 3
- 0
.eslintrc View File

@ -0,0 +1,3 @@
{
"extends": "@joepie91/eslint-config"
}

+ 1
- 0
.gitignore View File

@ -0,0 +1 @@
node_modules

+ 51
- 0
README.md View File

@ -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/
```

+ 133
- 0
bin/scriptless-svg View File

@ -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`)}`);
});

+ 25
- 0
package.json View File

@ -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"
}
}

BIN
screenshot.png View File

Before After
Width: 664  |  Height: 707  |  Size: 112 KiB

+ 1280
- 0
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save