1 |
|
2 | 'use strict';
|
3 |
|
4 | module.exports = function (grunt) {
|
5 |
|
6 | var retire = require('retire/lib/retire'),
|
7 | repo = require('retire/lib/repo'),
|
8 | resolve = require('retire/lib/resolve'),
|
9 | log = require('retire/lib/utils').log,
|
10 | scanner = require('retire/lib/scanner'),
|
11 | fs = require('fs'),
|
12 | path = require('path'),
|
13 | req = require('request'),
|
14 | os = require('os'),
|
15 | async = require('async');
|
16 |
|
17 | grunt.registerMultiTask('retire', 'Scanner detecting the use of JavaScript libraries with known vulnerabilites.', function () {
|
18 | var done = this.async();
|
19 | var jsRepo = null;
|
20 | var nodeRepo = null;
|
21 | var vulnsFound = false;
|
22 | var filesSrc = this.filesSrc;
|
23 | var request = req;
|
24 | var defaultIgnoreFile = '.retireignore';
|
25 | var output = {};
|
26 | var scanedFile;
|
27 |
|
28 | function taskVulnLogger(msg) {
|
29 | var keyValue;
|
30 | keyValue = scanedFile.slice(scanedFile.lastIndexOf('/') + 1);
|
31 |
|
32 | if (keyValue.indexOf('.') > -1) {
|
33 | keyValue = 'js.' + keyValue.slice(0, keyValue.indexOf('.'));
|
34 | }
|
35 | else {
|
36 | keyValue = 'node.' + keyValue;
|
37 | }
|
38 | if (!output.hasOwnProperty(keyValue)) {
|
39 | output[keyValue] = 1;
|
40 | }
|
41 | output[keyValue] = output[keyValue] + 1;
|
42 | return grunt.log.error(msg);
|
43 | }
|
44 |
|
45 |
|
46 | var options = this.options({
|
47 | verbose: true,
|
48 | packageOnly: false,
|
49 | jsRepository: 'https://raw.github.com/RetireJS/retire.js/master/repository/jsrepository.json',
|
50 | nodeRepository: 'https://raw.github.com/RetireJS/retire.js/master/repository/npmrepository.json',
|
51 | logger: grunt.log.writeln,
|
52 | warnlogger: taskVulnLogger,
|
53 | outputFile: false
|
54 | });
|
55 | var logger = log(options);
|
56 |
|
57 | if (!options.nocache) {
|
58 | options.cachedir = path.resolve(os.tmpdir(), '.retire-cache/');
|
59 | }
|
60 | var ignores = options.ignore ? options.ignore.split(',') : [];
|
61 | options.ignore = [];
|
62 | if (!options.ignorefile && grunt.file.exists(defaultIgnoreFile)) {
|
63 | options.ignorefile = defaultIgnoreFile;
|
64 | }
|
65 |
|
66 | if(options.ignorefile) {
|
67 | if (!grunt.file.exists(options.ignorefile)) {
|
68 | grunt.log.error('Error: Could not read ignore file: ' + options.ignorefile);
|
69 | process.exit(1);
|
70 | }
|
71 | var lines = fs.readFileSync(options.ignorefile).toString().split(/\r\n|\n/g).filter(function(e) { return e !== ''; });
|
72 | var ignored = lines.map(function(e) { return e[0] === '@' ? e.slice(1) : path.resolve(e); });
|
73 | options.ignore = options.ignore.concat(ignored);
|
74 | }
|
75 |
|
76 | ignores.forEach(function(e) { options.ignore.push(e); });
|
77 | logger.verbose("Ignoring " + JSON.stringify(options.ignore));
|
78 |
|
79 |
|
80 | grunt.verbose.writeflags(options, 'Options');
|
81 |
|
82 |
|
83 | scanner.on('vulnerable-dependency-found', function(e) {
|
84 | vulnsFound = true;
|
85 | });
|
86 | var events = [];
|
87 | function once(name, fun) {
|
88 | events.push(name);
|
89 | grunt.event.once(name, fun);
|
90 | }
|
91 | function on(name, fun) {
|
92 | events.push(name);
|
93 | grunt.event.on(name, fun);
|
94 | }
|
95 |
|
96 |
|
97 | once('retire-js-repo', function() {
|
98 | filesSrc.forEach(function(filepath) {
|
99 | if (!grunt.file.exists(filepath)) {
|
100 | grunt.log.debug('Skipping directory file:', filepath);
|
101 | return;
|
102 | }
|
103 | if (!grunt.file.isFile(filepath)) {
|
104 | grunt.log.debug('Not a file:', filepath);
|
105 | return;
|
106 | }
|
107 | scanedFile = filepath;
|
108 | if(options.verbose) {
|
109 | grunt.log.writeln('Checking:', filepath);
|
110 | }
|
111 | if (filepath.match(/\.js$/)) {
|
112 | scanner.scanJsFile(filepath, jsRepo, options);
|
113 | } else if (filepath.match(/\/bower.json$/)) {
|
114 | scanner.scanBowerFile(filepath, jsRepo, options);
|
115 | } else {
|
116 | grunt.log.debug('Unknown file type:', filepath);
|
117 | }
|
118 | });
|
119 | grunt.event.emit('retire-done');
|
120 | });
|
121 |
|
122 | on('retire-node-scan', function(filesSrc) {
|
123 | if (filesSrc.length === 0) {
|
124 | grunt.event.emit('retire-done');
|
125 | return;
|
126 | }
|
127 | var filepath = filesSrc[0];
|
128 | if(grunt.file.exists(filepath + '/package.json')) {
|
129 | scanedFile = filepath.slice( 0, filepath.lastIndexOf('/') );
|
130 | if(options.verbose) {
|
131 | grunt.log.writeln('Checking:', filepath);
|
132 | }
|
133 | resolve.getNodeDependencies(filepath, options.packageOnly).on('done', function(dependencies) {
|
134 | scanner.scanDependencies(dependencies, nodeRepo, options);
|
135 | grunt.event.emit('retire-node-scan', filesSrc.slice(1));
|
136 | });
|
137 | } else {
|
138 | grunt.log.debug('Skipping. Could not find:', filepath + '/package.json');
|
139 | grunt.event.emit('retire-node-scan', filesSrc.slice(1));
|
140 | }
|
141 | });
|
142 |
|
143 | once('retire-load-js', function() {
|
144 | var done = function (repo) {
|
145 | jsRepo = repo;
|
146 | grunt.event.emit('retire-js-repo');
|
147 | };
|
148 |
|
149 | if (/^http[s]?:/.test(options.jsRepository)) {
|
150 | repo.loadrepository(options.jsRepository, options).on('done', done);
|
151 | } else {
|
152 | repo.loadrepositoryFromFile(options.jsRepository, options).on('done', done);
|
153 | }
|
154 |
|
155 | });
|
156 |
|
157 | once('retire-load-node', function() {
|
158 | var done = function(repo) {
|
159 | nodeRepo = repo;
|
160 | grunt.event.emit('retire-node-scan', filesSrc);
|
161 | };
|
162 |
|
163 | if (/^http[s]?:/.test(options.nodeRepository)) {
|
164 | repo.loadrepository(options.nodeRepository, options).on('done', done);
|
165 | } else {
|
166 | repo.loadrepositoryFromFile(options.nodeRepository, options).on('done', done);
|
167 | }
|
168 | });
|
169 |
|
170 | once('retire-done', function() {
|
171 | if(!vulnsFound){
|
172 | grunt.log.writeln("No vulnerabilities found.");
|
173 | }
|
174 | events.forEach(function(e) {
|
175 | grunt.event.removeAllListeners(e);
|
176 | });
|
177 | if (options.outputFile) {
|
178 | grunt.file.write(options.outputFile, JSON.stringify(output));
|
179 | }
|
180 | done(!vulnsFound);
|
181 | });
|
182 |
|
183 | grunt.event.emit(this.target === 'node' ? 'retire-load-node' : 'retire-load-js');
|
184 |
|
185 | });
|
186 |
|
187 | };
|