Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 48 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -89,6 +95,41 @@ 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': ({ cmd, args, line, instruction }) => {
return cmd.toLowerCase() === 'add';
}
},

'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': ({ cmd, line, args}) => {
// This function doesn't care about full instruction so it omits if from arguments
return cmd.toLowerCase() === 'run' && args.match(/(curl|wget)[^|^>]*[|>]/);
}
},
}
```

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:

```
dockerfilelint -r path/to/ruleset.js path/to/Dockerfile
```

#### From a Docker container

(Replace the ``pwd``/Dockerfile with the path to your local Dockerfile)
Expand Down
15 changes: 11 additions & 4 deletions bin/dockerfilelint
Original file line number Diff line number Diff line change
Expand Up @@ -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 -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'
Expand All @@ -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 = '';
Expand Down Expand Up @@ -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 () {
Expand Down
36 changes: 34 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/) || [];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -294,6 +308,24 @@ function runLine(state, instructions, idx) {
}
}

if (state.customRuleset){
// For each custom rule, run the rule and push the result
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[1].function?.({
cmd: cmd,
args: args,
line: line,
instruction: instruction
});
if (result) {
items.push(messages.build(state.rules, rule[0], line));
}
}
}

// Remove any null items from the result
items = items.filter(function(n){ return n != null });

Expand Down
4 changes: 4 additions & 0 deletions lib/messages.js
Original file line number Diff line number Diff line change
@@ -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') {
Expand Down
3 changes: 3 additions & 0 deletions lib/reporter/cli_reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') + ' 👍');
Expand Down
7 changes: 6 additions & 1 deletion lib/reporter/json_reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions lib/reporter/reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
3 changes: 3 additions & 0 deletions test/examples/Dockerfile.custom-ruleset
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM scratch

RUN wget http://my_website/script.sh | sh
18 changes: 18 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
20 changes: 20 additions & 0 deletions test/ruleset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module.exports.rules = {
'add_prohibited': {
'title': 'ADD Command Prohibited',
'description': 'ADD command is not allowed! Use copy instead!',
'category': 'Optimization',
'function': ({ cmd, args, line, instruction }) => {
return cmd.toLowerCase() === 'add';
}
},

'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': ({ cmd, line, args}) => {
// This function doesn't care about full instruction so it omits if from arguments
return cmd.toLowerCase() === 'run' && args.match(/(curl|wget)[^|^>]*[|>]/);
}
},
}