From 562992e970090811dc9cf3df613641559a32974c Mon Sep 17 00:00:00 2001 From: Ava Thorn Date: Sat, 10 Apr 2021 00:11:17 -0400 Subject: [PATCH 1/5] added ability to provide custom rulesets --- README.md | 37 +++++++++++++++++++++++++ bin/dockerfilelint | 15 +++++++--- lib/index.js | 37 +++++++++++++++++++++++-- lib/messages.js | 4 +++ lib/reporter/cli_reporter.js | 3 ++ lib/reporter/json_reporter.js | 7 ++++- lib/reporter/reporter.js | 5 ++-- test/examples/Dockerfile.custom-ruleset | 3 ++ test/index.js | 18 ++++++++++++ test/ruleset.js | 24 ++++++++++++++++ 10 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 test/examples/Dockerfile.custom-ruleset create mode 100644 test/ruleset.js diff --git a/README.md b/README.md index deb8d36..7e7937e 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,43 @@ apt-get_missing_rm deprecated_in_1.13 ``` +### Custom Rulesets + +You can add your own custom rulesets by supplying the -r option to dockerfilelint. Simply create a ruleset.js file first. It must export the "rules" variable with the proper keys. Here is an example of a simple two-rule ruleset file. + +``` +module.exports.rules = { + 'add_prohibited': { + 'title': 'ADD Command Prohibited', + 'description': 'ADD command is not allowed! Use copy instead!', + 'category': 'Optimization', + 'function': ({ messages, state, cmd, args, line, instruction }) => { + if(cmd.toLowerCase() === 'add'){ + return messages.build(state.rules, 'add_prohibited', line); + } + } + }, + + 'avoid_curl_bashing': { + 'title': 'Avoid Curl Bashing', + 'description': 'Do not pipe bash or wget commands directly to shell. This is very insecure and can cause many issues with security. If you must, make sure to vet the script and verify its authenticity. E.G. "RUN wget http://my_website/script.sh | sh" is prohibited', + 'category': 'Optimization', + 'function': ({ messages, state, cmd, line, args}) => { + // This function doesn't care about full instruction so it omits if from arguments + if(cmd.toLowerCase() === 'run' && args.match(/(curl|wget)[^|^>]*[|>]/)){ + return messages.build(state.rules, 'avoid_curl_bashing', line); + } + } + }, +} +``` + +Then simply call dockerfilelint with the -r command to pass the ruleset file in: + +``` +dockerfilelint -r path/to/ruleset.js path/to/Dockerfile +``` + #### From a Docker container (Replace the ``pwd``/Dockerfile with the path to your local Dockerfile) diff --git a/bin/dockerfilelint b/bin/dockerfilelint index 4811479..dacb455 100755 --- a/bin/dockerfilelint +++ b/bin/dockerfilelint @@ -22,12 +22,18 @@ var argv = require('yargs') desc: 'Path for .dockerfilelintrc configuration file', type: 'string' }) + .option('r', { + alias: 'ruleset', + desc: 'Path for custom ruleset js file', + type: 'string' + }) .alias('v', 'version') .help().alias('h', 'help') .example('dockerfilelint Dockerfile', 'Lint a Dockerfile in the current working directory\n') .example('dockerfilelint test/example/* -j', 'Lint all files in the test/example directory and output results in JSON\n') .example(`dockerfilelint 'FROM latest'`, 'Lint the contents given as a string on the command line\n') .example('dockerfilelint < Dockerfile', 'Lint the contents of Dockerfile via stdin') + .example('dockerfilelint -c custom-ruleset.js Dockerfile', 'Lint the contents of Dockerfile using the default rules plus a set of custom rules defined in custom-ruleset.js') .wrap(86) .check(argv => { if (!argv.output && argv.json) argv.output = 'json' @@ -41,7 +47,7 @@ var chalk = require('chalk'); var Reporter = argv.output === 'json' ? require('../lib/reporter/json_reporter') : require('../lib/reporter/cli_reporter'); var reporter = new Reporter(); -var fileContent, configFilePath; +var fileContent, configFilePath, customRuleset; if (argv._.length === 0 || argv._[0] === '-') { // read content from stdin fileContent = ''; @@ -83,13 +89,14 @@ argv._.forEach((fileName) => { return process.exit(1); } - processContent(configFilePath, fileName, fileContent); + processContent(configFilePath, fileName, fileContent, argv.ruleset); }); report(); -function processContent (configFilePath, name, content) { - reporter.addFile(name, content, dockerfilelint.run(configFilePath, content)); +function processContent (configFilePath, name, content, customRulesetPath) { + const items = dockerfilelint.run(configFilePath, content, customRulesetPath); + reporter.addFile(name, content, items, customRulesetPath); } function report () { diff --git a/lib/index.js b/lib/index.js index dd7a408..c873e44 100644 --- a/lib/index.js +++ b/lib/index.js @@ -9,7 +9,7 @@ var apk = require('./apk'); var apt = require('./apt'); var messages = require('./messages'); -module.exports.run = function(configPath, content) { +module.exports.run = function(configPath, content, customRulesetPath) { // Parse the file into an array of instructions var instructions = {}; var lines = content.split(/\r?\n/) || []; @@ -56,9 +56,13 @@ module.exports.run = function(configPath, content) { cmdFound: false // this doesn't seem to be used anywhere }], items: [], - rules: loadRules(configPath) + rules: loadRules(configPath), + customRuleset: loadCustomRuleset(customRulesetPath) } + // Add custom ruleset to message bus + messages.addCustomRuleset(state.customRuleset); + for (var idx in instructions) { var result = runLine(state, instructions, idx); state.items = state.items.concat(result.items); @@ -101,6 +105,16 @@ function loadRules(configPath) { return rc.rules; } +function loadCustomRuleset(customRulesetPath){ + if (!customRulesetPath) return {}; + try{ + const absPath = path.join(process.cwd(), customRulesetPath) + return require(absPath)?.rules; + } catch (e) { + return {}; + } +} + function tryLoadFile(filename) { try { var stats = fs.lstatSync(filename); @@ -294,6 +308,25 @@ function runLine(state, instructions, idx) { } } + if (state.customRuleset){ + // For each custom rule, run the rule and push the result + for(const rule of Object.values(state.customRuleset)){ + // Rules are responsible for pushing to the message bus, thus send it here as an argument + // pass as a dictionary to allow users to only use the arguments that they want + const result = rule.function?.({ + messages: messages, + state: state, + cmd: cmd, + args: args, + line: line, + instruction: instruction + }); + if (result) { + items.push(result); + } + } + } + // Remove any null items from the result items = items.filter(function(n){ return n != null }); diff --git a/lib/messages.js b/lib/messages.js index 4b883b7..30824f2 100644 --- a/lib/messages.js +++ b/lib/messages.js @@ -1,6 +1,10 @@ var reference = require('./reference'); var messages = module.exports = { + addCustomRuleset: function(customRules) { + reference = { ...reference, ...customRules }; + }, + parseBool: function(s) { s = s.toLowerCase(); if (s === 'off' || s === 'false' || s === '0' || s === 'n') { diff --git a/lib/reporter/cli_reporter.js b/lib/reporter/cli_reporter.js index dfd0e6b..bf6e2b3 100644 --- a/lib/reporter/cli_reporter.js +++ b/lib/reporter/cli_reporter.js @@ -36,6 +36,9 @@ class CliReporter extends Reporter { self.ui.div( { text: 'File: ' + file, padding: PAD_TOP1_LEFT0 } ); + if(fileReport.customRulesetPath){ + self.ui.div('Custom Ruleset: ' + fileReport.customRulesetPath); + } let linesWithItems = Object.keys(fileReport.itemsByLine); if (linesWithItems.length === 0) { self.ui.div('Issues: ' + chalk.green('None found') + ' 👍'); diff --git a/lib/reporter/json_reporter.js b/lib/reporter/json_reporter.js index d2e2620..59f29fc 100644 --- a/lib/reporter/json_reporter.js +++ b/lib/reporter/json_reporter.js @@ -33,7 +33,12 @@ class JsonReporter extends Reporter { let fileReport = self.fileReports[file]; let linesWithItems = Object.keys(fileReport.itemsByLine); - let jsonReport = { file: file, issues_count: fileReport.uniqueIssues, issues: [] }; + let jsonReport = { + file: file, + issues_count: fileReport.uniqueIssues, + customRuleset: fileReport.customRulesetPath, + issues: [] + }; if (linesWithItems.length === 0) { reportFiles.push(jsonReport); diff --git a/lib/reporter/reporter.js b/lib/reporter/reporter.js index 3e3c926..b303447 100644 --- a/lib/reporter/reporter.js +++ b/lib/reporter/reporter.js @@ -8,13 +8,14 @@ class Reporter { } // group file items by line for easy reporting - addFile (file, fileContent, items) { + addFile (file, fileContent, items, customRulesetPath) { let self = this; if (!file) return self; let fileReport = self.fileReports[file] || { itemsByLine: {}, uniqueIssues: 0, - contentArray: (fileContent || '').replace('\r', '').split('\n') + contentArray: (fileContent || '').replace('\r', '').split('\n'), + customRulesetPath: customRulesetPath }; let ibl = fileReport.itemsByLine; [].concat(items).forEach((item) => { diff --git a/test/examples/Dockerfile.custom-ruleset b/test/examples/Dockerfile.custom-ruleset new file mode 100644 index 0000000..057d4f5 --- /dev/null +++ b/test/examples/Dockerfile.custom-ruleset @@ -0,0 +1,3 @@ +FROM scratch + +RUN wget http://my_website/script.sh | sh \ No newline at end of file diff --git a/test/index.js b/test/index.js index 4bb8ba1..d451b17 100644 --- a/test/index.js +++ b/test/index.js @@ -160,6 +160,24 @@ describe("index", function(){ }); }); + describe("#customRuleset", function() { + it("validates a custom ruleset can be supplied to extend rules", function() { + let expected = [{ + title: 'Avoid Curl Bashing', + line: 3, + rule: 'avoid_curl_bashing' + }] + let result = dockerfilelint.run('./test/examples', fs.readFileSync('./test/examples/Dockerfile.custom-ruleset', 'UTF-8'), './test/ruleset.js'); + _.forEach(result, function(r) { + delete r['function']; + delete r['description']; + delete r['category']; + }); + expect(result).to.have.length(expected.length); + expect(result).to.deep.equal(expected); + }); + }); + describe("#misc", function(){ it("validates the misc Dockerfile have the exact right issues reported", function(){ var expected = [ diff --git a/test/ruleset.js b/test/ruleset.js new file mode 100644 index 0000000..f8937ff --- /dev/null +++ b/test/ruleset.js @@ -0,0 +1,24 @@ +module.exports.rules = { + 'add_prohibited': { + 'title': 'ADD Command Prohibited', + 'description': 'ADD command is not allowed! Use copy instead!', + 'category': 'Optimization', + 'function': ({ messages, state, cmd, args, line, instruction }) => { + if(cmd.toLowerCase() === 'add'){ + return messages.build(state.rules, 'add_prohibited', line); + } + } + }, + + 'avoid_curl_bashing': { + 'title': 'Avoid Curl Bashing', + 'description': 'Do not pipe bash or wget commands directly to shell. This is very insecure and can cause many issues with security. If you must, make sure to vet the script and verify its authenticity. E.G. "RUN wget http://my_website/script.sh | sh" is prohibited', + 'category': 'Optimization', + 'function': ({ messages, state, cmd, line, args}) => { + // This function doesn't care about full instruction so it omits if from arguments + if(cmd.toLowerCase() === 'run' && args.match(/(curl|wget)[^|^>]*[|>]/)){ + return messages.build(state.rules, 'avoid_curl_bashing', line); + } + } + }, +} \ No newline at end of file From 19acd26387225389319ba3a4b65a0d35565f9314 Mon Sep 17 00:00:00 2001 From: Ava Thorn Date: Sat, 10 Apr 2021 00:19:21 -0400 Subject: [PATCH 2/5] fix typo in help doc --- bin/dockerfilelint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/dockerfilelint b/bin/dockerfilelint index dacb455..dad6113 100755 --- a/bin/dockerfilelint +++ b/bin/dockerfilelint @@ -33,7 +33,7 @@ var argv = require('yargs') .example('dockerfilelint test/example/* -j', 'Lint all files in the test/example directory and output results in JSON\n') .example(`dockerfilelint 'FROM latest'`, 'Lint the contents given as a string on the command line\n') .example('dockerfilelint < Dockerfile', 'Lint the contents of Dockerfile via stdin') - .example('dockerfilelint -c custom-ruleset.js Dockerfile', 'Lint the contents of Dockerfile using the default rules plus a set of custom rules defined in custom-ruleset.js') + .example('dockerfilelint -r custom-ruleset.js Dockerfile', 'Lint the contents of Dockerfile using the default rules plus a set of custom rules defined in custom-ruleset.js') .wrap(86) .check(argv => { if (!argv.output && argv.json) argv.output = 'json' From 65b70d1d430d82d98ea939b26b99a33d86bcf208 Mon Sep 17 00:00:00 2001 From: Ava Thorn Date: Sat, 10 Apr 2021 20:17:47 -0400 Subject: [PATCH 3/5] Update rules to return truthy or falsey --- README.md | 16 +++++++--------- lib/index.js | 11 +++++------ test/ruleset.js | 12 ++++-------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 7e7937e..87ce931 100644 --- a/README.md +++ b/README.md @@ -99,10 +99,8 @@ module.exports.rules = { 'title': 'ADD Command Prohibited', 'description': 'ADD command is not allowed! Use copy instead!', 'category': 'Optimization', - 'function': ({ messages, state, cmd, args, line, instruction }) => { - if(cmd.toLowerCase() === 'add'){ - return messages.build(state.rules, 'add_prohibited', line); - } + 'function': ({ cmd, args, line, instruction }) => { + return cmd.toLowerCase() === 'add'; } }, @@ -110,17 +108,17 @@ module.exports.rules = { 'title': 'Avoid Curl Bashing', 'description': 'Do not pipe bash or wget commands directly to shell. This is very insecure and can cause many issues with security. If you must, make sure to vet the script and verify its authenticity. E.G. "RUN wget http://my_website/script.sh | sh" is prohibited', 'category': 'Optimization', - 'function': ({ messages, state, cmd, line, args}) => { + 'function': ({ cmd, line, args}) => { // This function doesn't care about full instruction so it omits if from arguments - if(cmd.toLowerCase() === 'run' && args.match(/(curl|wget)[^|^>]*[|>]/)){ - return messages.build(state.rules, 'avoid_curl_bashing', line); - } + return cmd.toLowerCase() === 'run' && args.match(/(curl|wget)[^|^>]*[|>]/); } }, } ``` -Then simply call dockerfilelint with the -r command to pass the ruleset file in: +Notice, if a rule function return true, it will be sent as a message to the reporter, otherwise it will be ignored for that line. + +After you have your ruleset defined, simply call dockerfilelint with the -r command to pass the ruleset file in: ``` dockerfilelint -r path/to/ruleset.js path/to/Dockerfile diff --git a/lib/index.js b/lib/index.js index c873e44..da14351 100644 --- a/lib/index.js +++ b/lib/index.js @@ -310,19 +310,18 @@ function runLine(state, instructions, idx) { if (state.customRuleset){ // For each custom rule, run the rule and push the result - for(const rule of Object.values(state.customRuleset)){ - // Rules are responsible for pushing to the message bus, thus send it here as an argument + for(const rule of Object.entries(state.customRuleset)){ + // Rules simply should return true or false. This can also be interpreted as + // truthy or falsey. If true, send the message to the message bus. // pass as a dictionary to allow users to only use the arguments that they want - const result = rule.function?.({ - messages: messages, - state: state, + const result = rule[1].function?.({ cmd: cmd, args: args, line: line, instruction: instruction }); if (result) { - items.push(result); + items.push(messages.build(state.rules, rule[0], line)); } } } diff --git a/test/ruleset.js b/test/ruleset.js index f8937ff..3765339 100644 --- a/test/ruleset.js +++ b/test/ruleset.js @@ -3,10 +3,8 @@ module.exports.rules = { 'title': 'ADD Command Prohibited', 'description': 'ADD command is not allowed! Use copy instead!', 'category': 'Optimization', - 'function': ({ messages, state, cmd, args, line, instruction }) => { - if(cmd.toLowerCase() === 'add'){ - return messages.build(state.rules, 'add_prohibited', line); - } + 'function': ({ cmd, args, line, instruction }) => { + return cmd.toLowerCase() === 'add'; } }, @@ -14,11 +12,9 @@ module.exports.rules = { 'title': 'Avoid Curl Bashing', 'description': 'Do not pipe bash or wget commands directly to shell. This is very insecure and can cause many issues with security. If you must, make sure to vet the script and verify its authenticity. E.G. "RUN wget http://my_website/script.sh | sh" is prohibited', 'category': 'Optimization', - 'function': ({ messages, state, cmd, line, args}) => { + 'function': ({ cmd, line, args}) => { // This function doesn't care about full instruction so it omits if from arguments - if(cmd.toLowerCase() === 'run' && args.match(/(curl|wget)[^|^>]*[|>]/)){ - return messages.build(state.rules, 'avoid_curl_bashing', line); - } + return cmd.toLowerCase() === 'run' && args.match(/(curl|wget)[^|^>]*[|>]/); } }, } \ No newline at end of file From 6b8218db682aac39822ffe38bdf150b661be0d59 Mon Sep 17 00:00:00 2001 From: Ava Thorn Date: Sat, 10 Apr 2021 20:19:32 -0400 Subject: [PATCH 4/5] fixed typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 87ce931..2496fc8 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ module.exports.rules = { } ``` -Notice, if a rule function return true, it will be sent as a message to the reporter, otherwise it will be ignored for that line. +Notice, if a rule function returns true, it will be sent as a message to the reporter, otherwise it will be ignored for that line. After you have your ruleset defined, simply call dockerfilelint with the -r command to pass the ruleset file in: From b55b99cbd6fc165acd4c45e1552095264fc98496 Mon Sep 17 00:00:00 2001 From: Ava Thorn Date: Sat, 10 Apr 2021 20:59:56 -0400 Subject: [PATCH 5/5] Updated readme for updated help command --- README.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2496fc8..9b8bd3c 100644 --- a/README.md +++ b/README.md @@ -33,20 +33,26 @@ Options: -o, --output Specify the format to use for output of linting results. Valid values are `json` or `cli` (default). [string] -j, --json Output linting results as JSON, equivalent to `-o json`. [boolean] + -c, --config Path for .dockerfilelintrc configuration file [string] + -r, --ruleset Path for custom ruleset js file [string] -v, --version Show version number [boolean] -h, --help Show help [boolean] Examples: - dockerfilelint Dockerfile Lint a Dockerfile in the current working - directory + dockerfilelint Dockerfile Lint a Dockerfile in the current + working directory - dockerfilelint test/example/* -j Lint all files in the test/example directory and - output results in JSON + dockerfilelint test/example/* -j Lint all files in the test/example + directory and output results in JSON - dockerfilelint 'FROM latest' Lint the contents given as a string on the - command line + dockerfilelint 'FROM latest' Lint the contents given as a string on + the command line - dockerfilelint < Dockerfile Lint the contents of Dockerfile via stdin + dockerfilelint < Dockerfile Lint the contents of Dockerfile via + stdin + dockerfilelint -r custom-ruleset.js Lint the contents of Dockerfile using + Dockerfile the default rules plus a set of custom + rules defined in custom-ruleset.js ``` #### Configuring