From dba2ea1a3f760aa7066a7184517cfe26eb9e9f32 Mon Sep 17 00:00:00 2001 From: Baptiste LARVOL-SIMON Date: Wed, 6 Feb 2019 17:31:27 +0100 Subject: [PATCH 01/11] Add Cookies support --- index.js | 6 ++- lib/cookies.js | 128 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/tasks.js | 58 ++++++++++++++++++++-- package.json | 13 ++++- 4 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 lib/cookies.js diff --git a/index.js b/index.js index 176e02b..b6f5385 100755 --- a/index.js +++ b/index.js @@ -25,6 +25,7 @@ app .option('-a, --analytics', 'checks for Google Analytics & Piwik') .option('-t, --tracking', 'checks for Social Media tracking & embeds') .option('-c, --cdn', 'checks for Content Delivery Networks') + .option('-k, --cookies [expiration delay, in month]', 'checks for cookies lifetime (< 13 month by defaut)', false) //.option('-r, --recursive', 'tries to follow links to check every internal site', false) .action((url, args) => { // Error Handling @@ -46,9 +47,10 @@ app if (args.parent.mute) ui.set('silent'); // initialize the task runner - const tasks = new Tasks(url, ui); + const tasks = new Tasks(url, ui, args); if (args.ssl) tasks.new('ssl'); + if (args.cookies) tasks.new('cookies'); if (args.fonts) tasks.new('fonts'); if (args.prefetching) tasks.new('prefetching'); if (args.analytics) tasks.new('analytics'); @@ -72,4 +74,4 @@ app app.parse(process.argv); -//if (!app.args.length) app.help(); \ No newline at end of file +//if (!app.args.length) app.help(); diff --git a/lib/cookies.js b/lib/cookies.js new file mode 100644 index 0000000..af96f49 --- /dev/null +++ b/lib/cookies.js @@ -0,0 +1,128 @@ +/*! + * cookies (adaptated from server-side cookies from https://github.com/pillarjs/cookies) + * Copyright(c) 2014 Jed Schmidt, http://jed.is/ + * Copyright(c) 2015-2016 Douglas Christopher Wilson + * Copyright(c) 2018 Baptiste LARVOL-SIMON, http://www.e-glop.net/ + * MIT Licensed + */ + +'use strict' + +var deprecate = require('depd')('cookies') +var Keygrip = require('keygrip') +var cache = {} + +/** + * RegExp to match field-content in RFC 7230 sec 3.2 + * + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + * obs-text = %x80-FF + */ + +var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; + +/** + * RegExp to match Same-Site cookie attribute value. + */ + +var sameSiteRegExp = /^(?:lax|strict)$/i + +function Cookies(response){ + this.response = response; +} + +/** + * @return Array + **/ +Cookies.prototype.fetchAll = function() { + var header, cookies = []; + + header = this.response.headers["set-cookie"]; + if (!header) return []; + + header.forEach(cook => { + var name, + attrs = [], + data = cook.split(/\\{0}; /) + + name = data.shift().split('='); + data.forEach(attr => { + var dat = attr.split('='); + attrs[dat[0]] = dat[1]; + }); + + cookies.push(new Cookie(name[0], name[1], attrs)); + }); + + return cookies; +} + +function Cookie(name, value, attrs) { + if (!fieldContentRegExp.test(name)) { + throw new TypeError('argument name is invalid'); + } + + if (value && !fieldContentRegExp.test(value)) { + throw new TypeError('argument value is invalid'); + } + + value || (this.expires = new Date(0)) + + this.name = name + this.value = value || "" + + for (var name in attrs) { + switch ( name ) { + case 'secure': + this[name] = true; + break; + case 'expires': + this[name] = new Date(attrs[name]); + break; + default: + this[name] = attrs[name]; + } + } + + if (this.path && !fieldContentRegExp.test(this.path)) { + throw new TypeError('option path is invalid'); + } + + if (this.domain && !fieldContentRegExp.test(this.domain)) { + throw new TypeError('option domain is invalid'); + } + + if (this.sameSite && this.sameSite !== true && !sameSiteRegExp.test(this.sameSite)) { + throw new TypeError('option sameSite is invalid') + } +} + +Cookie.prototype.path = "/"; +Cookie.prototype.expires = undefined; +Cookie.prototype.domain = undefined; +Cookie.prototype.httpOnly = true; +Cookie.prototype.sameSite = false; +Cookie.prototype.secure = false; +Cookie.prototype.overwrite = false; + +// back-compat so maxage mirrors maxAge +Object.defineProperty(Cookie.prototype, 'maxage', { + configurable: true, + enumerable: true, + get: function () { return this.maxAge }, + set: function (val) { return this.maxAge = val } +}); +deprecate.property(Cookie.prototype, 'maxage', '"maxage"; use "maxAge" instead') + +function getPattern(name) { + if (cache[name]) return cache[name] + + return cache[name] = new RegExp( + "(?:^|;) *" + + name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") + + "=([^;]*)" + ) +} + +module.exports = Cookies diff --git a/lib/tasks.js b/lib/tasks.js index f559a29..9705a9d 100644 --- a/lib/tasks.js +++ b/lib/tasks.js @@ -8,9 +8,10 @@ const HTMLParser = require('./html-parser'); const FontsParser = require('./fonts-parser'); // const tools = require('./tools'); const UI = require('./ui'); +const Cookies = require('./cookies'); class Tasks { - constructor(url, uiInstance) { + constructor(url, uiInstance, args) { this.default_tasks = { normalize: { dependencies: [], @@ -29,6 +30,9 @@ class Tasks { dependencies: ['html', 'css', 'js'], mandatory: true }, + cookies: { + dependencies: ['html', 'css'] + }, ssl: { dependencies: ['html', 'css'] }, @@ -57,6 +61,7 @@ class Tasks { this.ui = (!uiInstance) ? new UI() : uiInstance; this.hp; + this.args = args; this.url = url; this.tasks = []; this.data = {}; @@ -69,13 +74,15 @@ class Tasks { /** * Adds a new Task to the list * @param {string} task + * @param {string} arg value * @class Tasks */ - new(task) { + new(task, value) { this.default_tasks[task].dependencies.forEach(dep => { if (this.tasks.indexOf(dep) === -1) this.tasks.push(dep); }); if (this.tasks.indexOf(task) === -1) this.tasks.push(task); + if ( value !== undefined ) this.value = value; } @@ -159,6 +166,7 @@ class Tasks { this.getPrefetchingInformation(); this.getAnalyticsInformation(); this.getCDNInformation(); + this.getCookiesInformation(); console.log(''); // console.log('Remaining: ', this.tasks); @@ -395,6 +403,50 @@ class Tasks { } + /** + * Gathers Cookies Information + * @class Tasks + */ + getCookiesInformation() { + if (!this.hasTask('cookies')) { + return; + } + + this.args.cookies = parseInt(this.args.cookies,10) ? parseInt(this.args.cookies,10) : 13; + + // go away if no cookie is given + if (typeof this.data.html.headers['set-cookie'] != 'object') { + return; + } + + var getter = new Cookies(this.data.html); + var cookies = getter.fetchAll(); + + if ( cookies.length == 0 ) { + this.ui.headline('No cookie set'); + this.remove('cookies'); + return; + } + + var strtotime = require('locutus/php/datetime/strtotime'); + + this.ui.headline('Cookies'); + for ( var i in cookies ) { + var t1 = cookies[i].expires.getTime(), + t2 = new Date(strtotime('+'+this.args.cookies+' month 1 hour')*1000).getTime(); + + if ( t1 > t2 ) { + this.ui.error('Cookie "'+cookies[i].name+'" expires in more than '+this.args.cookies+' month (expires on '+cookies[i].expires.toLocaleDateString()+')', true); + continue; + } + this.ui.listitem(cookies[i].name, 'Expires on '+cookies[i].expires.toLocaleDateString()+' and '+(cookies[i].secure ? 'secure' : 'unsecure')); + } + + this.remove('cookies'); + return; + } + + /** * Gathes General Information (meta data) of the Website * @class Tasks @@ -670,4 +722,4 @@ class Tasks { } } -module.exports = Tasks; \ No newline at end of file +module.exports = Tasks; diff --git a/package.json b/package.json index 85d1514..48987fd 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,22 @@ "cliui": "^4.1.0", "commander": "^2.15.1", "css": "^2.2.3", + "depd": "~1.1.2", "fs-extra": "^6.0.1", "get-ssl-certificate": "^2.1.2", "got": "^8.3.1", + "keygrip": "~1.0.3", + "locutus": "^2.0.10", "moment": "^2.22.1", "rootpath": "^0.1.2" }, "devDependencies": { - "ava": "^0.25.0" + "ava": "^0.25.0", + "eslint": "3.19.0", + "express": "4.16.4", + "istanbul": "0.4.5", + "mocha": "5.2.0", + "restify": "6.4.0", + "supertest": "3.3.0" } -} \ No newline at end of file +} From daf2ce607fbf7901c4bd325212e2cd82d037197c Mon Sep 17 00:00:00 2001 From: Baptiste LARVOL-SIMON Date: Wed, 6 Feb 2019 18:02:53 +0100 Subject: [PATCH 02/11] add preliminary changes --- index.js | 2 ++ lib/tasks.js | 21 +++++++++++++++++++++ test.js | 7 +++++++ 3 files changed, 30 insertions(+) create mode 100644 test.js diff --git a/index.js b/index.js index b6f5385..b62105d 100755 --- a/index.js +++ b/index.js @@ -13,6 +13,7 @@ const ui = new UI(); // initialize the UI app .version(require('./package.json').version, '-V, --version') .option('-v, --verbose', 'shows you every single step') + .option('-z, --nfz', 'displays informations related to website report operating procedure, AKA NF Z67-147 in France.') .option('-m, --mute', 'shows only the results of the analysis'); app @@ -49,6 +50,7 @@ app // initialize the task runner const tasks = new Tasks(url, ui, args); + if (args.parent.nfz) tasks.new('nfz'); if (args.ssl) tasks.new('ssl'); if (args.cookies) tasks.new('cookies'); if (args.fonts) tasks.new('fonts'); diff --git a/lib/tasks.js b/lib/tasks.js index 9705a9d..bfdcea9 100644 --- a/lib/tasks.js +++ b/lib/tasks.js @@ -30,6 +30,9 @@ class Tasks { dependencies: ['html', 'css', 'js'], mandatory: true }, + nfz: { + dependencies: ['html', 'css', 'js'], + }, cookies: { dependencies: ['html', 'css'] }, @@ -160,6 +163,7 @@ class Tasks { // console.log('Remaining: ', this.tasks); this.getGeneralInformation(); + this.getNFZInformations(); this.getSSLInformation(); this.getFontInformation(); this.getSocialMediaInformation(); @@ -447,6 +451,23 @@ class Tasks { } + /** + * Gathes NF Z67-147 informations (meta data) about the audit + * @class Tasks + */ + getNFZInformations() { + if (!this.hasTask('nfz')) { + console.error('z67 out'); + return; + } + + this.ui.headline('NF Z67-147 informations about the current audit'); + + const os = require('os') + this.ui.listitem('Operating system', os.type()+' '+os.release()+' '+os.arch()); + // etc. + } + /** * Gathes General Information (meta data) of the Website * @class Tasks diff --git a/test.js b/test.js new file mode 100644 index 0000000..b6636a5 --- /dev/null +++ b/test.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node --harmony + +'use strict' + +const os = require('os'); + +console.error(os.type(), os.release(), os.arch()); From b038e2af36a46092c20ab34e3f8c6ae98660ed25 Mon Sep 17 00:00:00 2001 From: Baptiste LARVOL-SIMON Date: Thu, 7 Feb 2019 09:25:46 +0100 Subject: [PATCH 03/11] Going further into NF Z67-147 + preparation of further auditing --- index.js | 4 +- lib/recommendation-collection.js | 39 +++++++++++++++++ lib/tasks.js | 72 +++++++++++++++++++++++++++++--- package.json | 1 + test.js | 22 ++++++++++ 5 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 lib/recommendation-collection.js diff --git a/index.js b/index.js index b62105d..569846a 100755 --- a/index.js +++ b/index.js @@ -14,7 +14,8 @@ app .version(require('./package.json').version, '-V, --version') .option('-v, --verbose', 'shows you every single step') .option('-z, --nfz', 'displays informations related to website report operating procedure, AKA NF Z67-147 in France.') - .option('-m, --mute', 'shows only the results of the analysis'); + .option('-a, --audit', 'displays recommandations for further auditing') + .option('-m, --mute', 'shows only the results of the analysis') app .command('scan [url]') @@ -51,6 +52,7 @@ app const tasks = new Tasks(url, ui, args); if (args.parent.nfz) tasks.new('nfz'); + if (args.parent.audit) tasks.new('audit'); if (args.ssl) tasks.new('ssl'); if (args.cookies) tasks.new('cookies'); if (args.fonts) tasks.new('fonts'); diff --git a/lib/recommendation-collection.js b/lib/recommendation-collection.js new file mode 100644 index 0000000..ed8cd05 --- /dev/null +++ b/lib/recommendation-collection.js @@ -0,0 +1,39 @@ +/* + * recommendation-collection + * Copyright(c) 2018 Baptiste LARVOL-SIMON, http://www.e-glop.net/ + * MIT Licensed + */ + +'use strict' + +function RecommendationCollection(){ +} + +RecommendationCollection.prototype.collection = {}; + +/** + * @return void + **/ +RecommendationCollection.prototype.add = function(key, value) { + if ( !Array.isArray(this.collection[key]) ) { + this.collection[key] = [] + } + + this.collection[key].push(value); +} + +/** + * @return Array + **/ +RecommendationCollection.prototype.getTopics = function() { + return Object.keys(this.collection); +} + +/** + * @return Array + **/ +RecommendationCollection.prototype.getWarningsFor = function(key) { + return this.collection[key]; +} + +module.exports = RecommendationCollection; diff --git a/lib/tasks.js b/lib/tasks.js index bfdcea9..ed31aac 100644 --- a/lib/tasks.js +++ b/lib/tasks.js @@ -9,6 +9,7 @@ const FontsParser = require('./fonts-parser'); // const tools = require('./tools'); const UI = require('./ui'); const Cookies = require('./cookies'); +const RecommendationCollection = require('./recommendation-collection'); class Tasks { constructor(url, uiInstance, args) { @@ -31,7 +32,10 @@ class Tasks { mandatory: true }, nfz: { - dependencies: ['html', 'css', 'js'], + dependencies: [], + }, + audit: { + dependencies: [], }, cookies: { dependencies: ['html', 'css'] @@ -68,6 +72,7 @@ class Tasks { this.url = url; this.tasks = []; this.data = {}; + this.recommendations = new RecommendationCollection(); // Put mandatory tasks already in the task list this.tasks = this.tasks.concat(this.getMandatoryTasks()); @@ -171,6 +176,7 @@ class Tasks { this.getAnalyticsInformation(); this.getCDNInformation(); this.getCookiesInformation(); + this.getAuditInformations(); console.log(''); // console.log('Remaining: ', this.tasks); @@ -192,6 +198,11 @@ class Tasks { this.data.social.fb_graph = social.hasFacebookSocialGraph(this.data.js); this.data.social.pinterest = social.hasPinterest(this.data.js); + for ( var sm in this.data.social ) { + if ( !this.data.social[sm] ) continue; + this.recommendations.add('Social medias processors', sm); + } + //console.log('FB CONNECT:', this.data.social.fb_connect); //console.log('FB SOCIAL GRAPH:', this.data.social.fb_graph); //console.log('PINTEREST:', this.data.social.pinterest); @@ -440,6 +451,7 @@ class Tasks { t2 = new Date(strtotime('+'+this.args.cookies+' month 1 hour')*1000).getTime(); if ( t1 > t2 ) { + this.recommendations.add('Cookies', 'lifetime'); this.ui.error('Cookie "'+cookies[i].name+'" expires in more than '+this.args.cookies+' month (expires on '+cookies[i].expires.toLocaleDateString()+')', true); continue; } @@ -456,16 +468,63 @@ class Tasks { * @class Tasks */ getNFZInformations() { - if (!this.hasTask('nfz')) { - console.error('z67 out'); - return; - } + if (!this.hasTask('nfz')) return; this.ui.headline('NF Z67-147 informations about the current audit'); const os = require('os') + const pkg = require('../package.json'); this.ui.listitem('Operating system', os.type()+' '+os.release()+' '+os.arch()); - // etc. + this.ui.listitem('Software', pkg.name+'-'+pkg.version+' - '+pkg.description); + this.ui.listitem('Web cache', 'Voided'); + this.ui.listitem('Web cookies', 'Voided'); + this.ui.listitem('Web proxy', 'Null'); + this.ui.listitem('Date & time', new Date().toLocaleString()); + + const dns = require('dns'); + const tld = this.url.replace(/^https{0,1}:\/\/([^\/]+)\/.*$/, '$1'); + var tasks = this; + + dns.resolve(tld, 'A', function(err, addresses){ + if ( err !== null ) return; + tasks.ui.listitem('Website IPv4', addresses); + }); + dns.resolve(tld, 'AAAA', function(err, addresses){ + if ( err !== null ) return; + tasks.ui.listitem('Website IPv6', addresses); + }); + dns.resolve('resolver1.opendns.com', 'A', function(err, addresses){ + dns.setServers(addresses); + dns.resolve('myip.opendns.com', 'A', function(err, addresses){ + if ( err !== null ) return; + tasks.ui.listitem("Auditor's IPv4", addresses); + }); + dns.resolve('myip.opendns.com', 'AAAA', function(err, addresses){ + if ( err !== null ) return; + tasks.ui.listitem("Auditor's IPv6", addresses); + }); + }); + } + + /** + * Gathes further recommendations for further human audit + * @class Tasks + */ + getAuditInformations() { + if (!this.hasTask('audit')) { + console.error('no recommendation expected'); + return; + } + + this.ui.headline('Recommendations for further human audit'); + + var topics = this.recommendations.getTopics(); + for ( var i in topics ) { + var recos = this.recommendations.getWarningsFor(topics[i]); + recos.forEach(reco => { + this.ui.listitem(topics[i], 'Check '+reco); + }); + } } /** @@ -744,3 +803,4 @@ class Tasks { } module.exports = Tasks; + diff --git a/package.json b/package.json index 48987fd..af0b1dd 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "repository": "https://github.com/mirkoschubert/gdpr-check.git", "author": "Mirko Schubert ", + "contributors": ["Baptiste LARVOL-SIMON (http://www.e-glop.net/)"], "license": "MIT", "dependencies": { "chalk": "^2.4.1", diff --git a/test.js b/test.js index b6636a5..29e4b34 100644 --- a/test.js +++ b/test.js @@ -2,6 +2,28 @@ 'use strict' +const arr = []; +console.log(typeof arr); + +/* const os = require('os'); +const pkg = require('./package.json'); console.error(os.type(), os.release(), os.arch()); +console.error(pkg.name, pkg.version, pkg.description); +console.error(new Date().toLocaleString()); + +const dns = require('dns'); +const url = 'http://myurl.tld/glop'; +console.error(url.replace(/^https{0,1}:\/\/([^\/]+)\/.*$/, '$1')); +//dns.resolve('resolver1.opendns.com', 'A', function(err, addresses){ + +/* +dns.resolve('resolver1.opendns.com', 'A', function(err, addresses){ + console.error(err, addresses); + dns.setServers(addresses); + dns.resolve('myip.opendns.com', 'A', function(err, addresses){ + console.error(err, addresses); + }); +}) +*/ From dfe5964d2d99ccb3c2a8c3b6783df3557d807fb4 Mon Sep 17 00:00:00 2001 From: Baptiste LARVOL-SIMON Date: Thu, 7 Feb 2019 17:03:22 +0100 Subject: [PATCH 04/11] rollback to NF Z67-147 only --- index.js | 2 -- lib/recommendation-collection.js | 39 -------------------------------- lib/tasks.js | 26 --------------------- 3 files changed, 67 deletions(-) delete mode 100644 lib/recommendation-collection.js diff --git a/index.js b/index.js index 569846a..b07a9cd 100755 --- a/index.js +++ b/index.js @@ -14,7 +14,6 @@ app .version(require('./package.json').version, '-V, --version') .option('-v, --verbose', 'shows you every single step') .option('-z, --nfz', 'displays informations related to website report operating procedure, AKA NF Z67-147 in France.') - .option('-a, --audit', 'displays recommandations for further auditing') .option('-m, --mute', 'shows only the results of the analysis') app @@ -52,7 +51,6 @@ app const tasks = new Tasks(url, ui, args); if (args.parent.nfz) tasks.new('nfz'); - if (args.parent.audit) tasks.new('audit'); if (args.ssl) tasks.new('ssl'); if (args.cookies) tasks.new('cookies'); if (args.fonts) tasks.new('fonts'); diff --git a/lib/recommendation-collection.js b/lib/recommendation-collection.js deleted file mode 100644 index ed8cd05..0000000 --- a/lib/recommendation-collection.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * recommendation-collection - * Copyright(c) 2018 Baptiste LARVOL-SIMON, http://www.e-glop.net/ - * MIT Licensed - */ - -'use strict' - -function RecommendationCollection(){ -} - -RecommendationCollection.prototype.collection = {}; - -/** - * @return void - **/ -RecommendationCollection.prototype.add = function(key, value) { - if ( !Array.isArray(this.collection[key]) ) { - this.collection[key] = [] - } - - this.collection[key].push(value); -} - -/** - * @return Array - **/ -RecommendationCollection.prototype.getTopics = function() { - return Object.keys(this.collection); -} - -/** - * @return Array - **/ -RecommendationCollection.prototype.getWarningsFor = function(key) { - return this.collection[key]; -} - -module.exports = RecommendationCollection; diff --git a/lib/tasks.js b/lib/tasks.js index ed31aac..36e2a52 100644 --- a/lib/tasks.js +++ b/lib/tasks.js @@ -34,9 +34,6 @@ class Tasks { nfz: { dependencies: [], }, - audit: { - dependencies: [], - }, cookies: { dependencies: ['html', 'css'] }, @@ -72,7 +69,6 @@ class Tasks { this.url = url; this.tasks = []; this.data = {}; - this.recommendations = new RecommendationCollection(); // Put mandatory tasks already in the task list this.tasks = this.tasks.concat(this.getMandatoryTasks()); @@ -176,7 +172,6 @@ class Tasks { this.getAnalyticsInformation(); this.getCDNInformation(); this.getCookiesInformation(); - this.getAuditInformations(); console.log(''); // console.log('Remaining: ', this.tasks); @@ -506,27 +501,6 @@ class Tasks { }); } - /** - * Gathes further recommendations for further human audit - * @class Tasks - */ - getAuditInformations() { - if (!this.hasTask('audit')) { - console.error('no recommendation expected'); - return; - } - - this.ui.headline('Recommendations for further human audit'); - - var topics = this.recommendations.getTopics(); - for ( var i in topics ) { - var recos = this.recommendations.getWarningsFor(topics[i]); - recos.forEach(reco => { - this.ui.listitem(topics[i], 'Check '+reco); - }); - } - } - /** * Gathes General Information (meta data) of the Website * @class Tasks From 9011bdf00fe86075d4ef4e73493bde5e75e6be7b Mon Sep 17 00:00:00 2001 From: Baptiste LARVOL-SIMON Date: Thu, 7 Feb 2019 09:25:46 +0100 Subject: [PATCH 05/11] Add conclusions as recommendations for further human auditing --- index.js | 2 ++ lib/recommendation-collection.js | 39 ++++++++++++++++++++++++++++++++ lib/tasks.js | 29 ++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 lib/recommendation-collection.js diff --git a/index.js b/index.js index b07a9cd..8e72cff 100755 --- a/index.js +++ b/index.js @@ -14,6 +14,7 @@ app .version(require('./package.json').version, '-V, --version') .option('-v, --verbose', 'shows you every single step') .option('-z, --nfz', 'displays informations related to website report operating procedure, AKA NF Z67-147 in France.') + .option('-u, --audit', 'displays recommandations for further auditing') .option('-m, --mute', 'shows only the results of the analysis') app @@ -51,6 +52,7 @@ app const tasks = new Tasks(url, ui, args); if (args.parent.nfz) tasks.new('nfz'); + if (args.parent.audit) tasks.new('audit'); if (args.ssl) tasks.new('ssl'); if (args.cookies) tasks.new('cookies'); if (args.fonts) tasks.new('fonts'); diff --git a/lib/recommendation-collection.js b/lib/recommendation-collection.js new file mode 100644 index 0000000..ed8cd05 --- /dev/null +++ b/lib/recommendation-collection.js @@ -0,0 +1,39 @@ +/* + * recommendation-collection + * Copyright(c) 2018 Baptiste LARVOL-SIMON, http://www.e-glop.net/ + * MIT Licensed + */ + +'use strict' + +function RecommendationCollection(){ +} + +RecommendationCollection.prototype.collection = {}; + +/** + * @return void + **/ +RecommendationCollection.prototype.add = function(key, value) { + if ( !Array.isArray(this.collection[key]) ) { + this.collection[key] = [] + } + + this.collection[key].push(value); +} + +/** + * @return Array + **/ +RecommendationCollection.prototype.getTopics = function() { + return Object.keys(this.collection); +} + +/** + * @return Array + **/ +RecommendationCollection.prototype.getWarningsFor = function(key) { + return this.collection[key]; +} + +module.exports = RecommendationCollection; diff --git a/lib/tasks.js b/lib/tasks.js index 36e2a52..6cb3baa 100644 --- a/lib/tasks.js +++ b/lib/tasks.js @@ -34,6 +34,9 @@ class Tasks { nfz: { dependencies: [], }, + audit: { + dependencies: [], + }, cookies: { dependencies: ['html', 'css'] }, @@ -69,6 +72,7 @@ class Tasks { this.url = url; this.tasks = []; this.data = {}; + this.recommendations = new RecommendationCollection(); // Put mandatory tasks already in the task list this.tasks = this.tasks.concat(this.getMandatoryTasks()); @@ -172,6 +176,7 @@ class Tasks { this.getAnalyticsInformation(); this.getCDNInformation(); this.getCookiesInformation(); + this.getAuditInformations(); console.log(''); // console.log('Remaining: ', this.tasks); @@ -499,6 +504,30 @@ class Tasks { tasks.ui.listitem("Auditor's IPv6", addresses); }); }); +<<<<<<< HEAD +======= + } + + /** + * Gathes further recommendations for further human audit + * @class Tasks + */ + getAuditInformations() { + if (!this.hasTask('audit')) { + console.error('no recommendation expected'); + return; + } + + this.ui.headline('Recommendations for further human audit'); + + var topics = this.recommendations.getTopics(); + for ( var i in topics ) { + var recos = this.recommendations.getWarningsFor(topics[i]); + recos.forEach(reco => { + this.ui.listitem(topics[i], 'Check '+reco); + }); + } +>>>>>>> b038e2a... Going further into NF Z67-147 + preparation of further auditing } /** From 2a912bdf66311cd6c57f496d15315b7ebcf1b570 Mon Sep 17 00:00:00 2001 From: Baptiste LARVOL-SIMON Date: Thu, 7 Feb 2019 17:46:16 +0100 Subject: [PATCH 06/11] Remove any NFZ67-147 reference to harden the ability to merge with upstream --- index.js | 2 -- lib/tasks.js | 51 --------------------------------------------------- 2 files changed, 53 deletions(-) diff --git a/index.js b/index.js index 8e72cff..026e9ee 100755 --- a/index.js +++ b/index.js @@ -13,7 +13,6 @@ const ui = new UI(); // initialize the UI app .version(require('./package.json').version, '-V, --version') .option('-v, --verbose', 'shows you every single step') - .option('-z, --nfz', 'displays informations related to website report operating procedure, AKA NF Z67-147 in France.') .option('-u, --audit', 'displays recommandations for further auditing') .option('-m, --mute', 'shows only the results of the analysis') @@ -51,7 +50,6 @@ app // initialize the task runner const tasks = new Tasks(url, ui, args); - if (args.parent.nfz) tasks.new('nfz'); if (args.parent.audit) tasks.new('audit'); if (args.ssl) tasks.new('ssl'); if (args.cookies) tasks.new('cookies'); diff --git a/lib/tasks.js b/lib/tasks.js index 6cb3baa..4316430 100644 --- a/lib/tasks.js +++ b/lib/tasks.js @@ -31,9 +31,6 @@ class Tasks { dependencies: ['html', 'css', 'js'], mandatory: true }, - nfz: { - dependencies: [], - }, audit: { dependencies: [], }, @@ -168,7 +165,6 @@ class Tasks { // console.log('Remaining: ', this.tasks); this.getGeneralInformation(); - this.getNFZInformations(); this.getSSLInformation(); this.getFontInformation(); this.getSocialMediaInformation(); @@ -462,52 +458,6 @@ class Tasks { return; } - - /** - * Gathes NF Z67-147 informations (meta data) about the audit - * @class Tasks - */ - getNFZInformations() { - if (!this.hasTask('nfz')) return; - - this.ui.headline('NF Z67-147 informations about the current audit'); - - const os = require('os') - const pkg = require('../package.json'); - this.ui.listitem('Operating system', os.type()+' '+os.release()+' '+os.arch()); - this.ui.listitem('Software', pkg.name+'-'+pkg.version+' - '+pkg.description); - this.ui.listitem('Web cache', 'Voided'); - this.ui.listitem('Web cookies', 'Voided'); - this.ui.listitem('Web proxy', 'Null'); - this.ui.listitem('Date & time', new Date().toLocaleString()); - - const dns = require('dns'); - const tld = this.url.replace(/^https{0,1}:\/\/([^\/]+)\/.*$/, '$1'); - var tasks = this; - - dns.resolve(tld, 'A', function(err, addresses){ - if ( err !== null ) return; - tasks.ui.listitem('Website IPv4', addresses); - }); - dns.resolve(tld, 'AAAA', function(err, addresses){ - if ( err !== null ) return; - tasks.ui.listitem('Website IPv6', addresses); - }); - dns.resolve('resolver1.opendns.com', 'A', function(err, addresses){ - dns.setServers(addresses); - dns.resolve('myip.opendns.com', 'A', function(err, addresses){ - if ( err !== null ) return; - tasks.ui.listitem("Auditor's IPv4", addresses); - }); - dns.resolve('myip.opendns.com', 'AAAA', function(err, addresses){ - if ( err !== null ) return; - tasks.ui.listitem("Auditor's IPv6", addresses); - }); - }); -<<<<<<< HEAD -======= - } - /** * Gathes further recommendations for further human audit * @class Tasks @@ -527,7 +477,6 @@ class Tasks { this.ui.listitem(topics[i], 'Check '+reco); }); } ->>>>>>> b038e2a... Going further into NF Z67-147 + preparation of further auditing } /** From 2b181e1888517cc68e1602591c5a5afbbcb60d07 Mon Sep 17 00:00:00 2001 From: Baptiste LARVOL-SIMON Date: Thu, 7 Feb 2019 18:41:57 +0100 Subject: [PATCH 07/11] done --- lib/tasks.js | 99 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 41 deletions(-) diff --git a/lib/tasks.js b/lib/tasks.js index 4316430..0c7452e 100644 --- a/lib/tasks.js +++ b/lib/tasks.js @@ -196,14 +196,13 @@ class Tasks { for ( var sm in this.data.social ) { if ( !this.data.social[sm] ) continue; - this.recommendations.add('Social medias processors', sm); + this.recommendations.add('Social medias', sm); } //console.log('FB CONNECT:', this.data.social.fb_connect); //console.log('FB SOCIAL GRAPH:', this.data.social.fb_graph); //console.log('PINTEREST:', this.data.social.pinterest); - this.remove('social'); } } @@ -236,9 +235,12 @@ class Tasks { this.ui.info(chalk.yellow(this.data.html.url.hostname) + ' uses ' + chalk.red(found.length) + ' Content Delivery Networks.\n'); found.forEach((cdn, i) => { this.ui.tableitem( - [chalk.yellow(String(i + 1) + '.'), cdn.host, chalk.dim(cdn.description)]); + [chalk.yellow(String(i + 1) + '.'), cdn.host, chalk.dim(cdn.description)] + ); }); + this.recommendations.add('Third Party', 'Content Delivery Networks'); } else this.ui.info('No Content Delivery Networks have been found.'); + this.remove('cdn'); } } @@ -255,8 +257,11 @@ class Tasks { this.ui.headline('Analytics'); if (typeof this.data.analytics.ga === 'undefined' && typeof this.data.analytics.gtag === 'undefined' && - typeof this.data.analytics.piwik === 'undefined' && typeof this.data.analytics.wordpress === 'undefined') + typeof this.data.analytics.piwik === 'undefined' && typeof this.data.analytics.wordpress === 'undefined') { this.ui.info('No Analytics Tool has been found.'); + this.remove('analytics'); + return; + } if (typeof this.data.analytics.ga !== 'undefined') { this.ui.info(chalk.red('Google Analytics') + ' has been found.\n'); @@ -283,6 +288,7 @@ class Tasks { ' (Jetpack) has been found.'); } + this.recommendations.add('Third party', 'analytics - web statistics'); this.remove('analytics'); } } @@ -296,37 +302,40 @@ class Tasks { if (dns.length === 0) { this.ui.info( chalk.yellow(this.data.html.url.hostname) + - ' supports no DNS prefetching.') - } else { - this.ui.info( - chalk.yellow(this.data.html.url.hostname) + ' has ' + - chalk.red(dns.length) + ' DNS prefetching elements.\n'); - this.ui.settable({ - cells: 3, - widths: [5, 30, 0] - }); - - dns.forEach((url, i) => { - let u = (url.startsWith('//')) ? url.replace('//', '') : url; - let explanation = ''; - - if (u.indexOf('fonts.googleapis.com') >= 0) explanation = 'Google Fonts'; - if (u.indexOf('gravatar.com') >= 0) explanation = 'Automattic Gravatar Service'; - if (u.indexOf('s.w.org') >= 0) explanation = 'WordPress Emojis CDN'; - if (u.match(/s[0-9]\.wp\.com/)) explanation = 'WordPress Styles CDN'; - if (u.match(/i[0-9]\.wp\.com/)) explanation = 'WordPress Images CDN'; - if (u.match(/v[0-9]\.wordpress\.com/)) explanation = 'WordPress Videos CDN'; - if (u.indexOf('maxcdn.bootstrapcdn.com') >= 0) explanation = 'Bootstrap CDN'; - if (u.indexOf('checkout.stripe.com') >= 0) explanation = 'Stripe Online Payments'; - if (u.indexOf('code.jquery.com') >= 0) explanation = 'jQuery CDN'; - if (u.indexOf('translate.google.com') >= 0) explanation = 'Google Translate'; - if (u.indexOf('use.typekit.net') >= 0) explanation = 'Adobe Typekit Web Fonts'; - if (u.indexOf('use.fontawesome.com') >= 0) explanation = 'Font Awesome CDN'; - - this.ui.tableitem( - [chalk.yellow(String(i + 1) + '.'), u, chalk.dim(explanation)]) - }); + ' supports no DNS prefetching.'); + this.remove('prefetching'); + return; } + + this.ui.info( + chalk.yellow(this.data.html.url.hostname) + ' has ' + + chalk.red(dns.length) + ' DNS prefetching elements.\n'); + this.ui.settable({ + cells: 3, + widths: [5, 30, 0] + }); + + dns.forEach((url, i) => { + let u = (url.startsWith('//')) ? url.replace('//', '') : url; + let explanation = ''; + + if (u.indexOf('fonts.googleapis.com') >= 0) explanation = 'Google Fonts'; + if (u.indexOf('gravatar.com') >= 0) explanation = 'Automattic Gravatar Service'; + if (u.indexOf('s.w.org') >= 0) explanation = 'WordPress Emojis CDN'; + if (u.match(/s[0-9]\.wp\.com/)) explanation = 'WordPress Styles CDN'; + if (u.match(/i[0-9]\.wp\.com/)) explanation = 'WordPress Images CDN'; + if (u.match(/v[0-9]\.wordpress\.com/)) explanation = 'WordPress Videos CDN'; + if (u.indexOf('maxcdn.bootstrapcdn.com') >= 0) explanation = 'Bootstrap CDN'; + if (u.indexOf('checkout.stripe.com') >= 0) explanation = 'Stripe Online Payments'; + if (u.indexOf('code.jquery.com') >= 0) explanation = 'jQuery CDN'; + if (u.indexOf('translate.google.com') >= 0) explanation = 'Google Translate'; + if (u.indexOf('use.typekit.net') >= 0) explanation = 'Adobe Typekit Web Fonts'; + if (u.indexOf('use.fontawesome.com') >= 0) explanation = 'Font Awesome CDN'; + + this.ui.tableitem( + [chalk.yellow(String(i + 1) + '.'), u, chalk.dim(explanation)]) + }); + this.recommendations.add('Third party', 'DNS Prefetching'); this.remove('prefetching'); } @@ -368,6 +377,7 @@ class Tasks { }); if (todo.length !== 0) { + var reco = false; todo.forEach((el, i) => { let count = Fonts.countFonts(this.data.fonts[el.type]); if (i > 0) console.log(''); @@ -375,7 +385,9 @@ class Tasks { count.forEach((f, j) => { this.ui.tableitem([chalk.yellow(String(j + 1) + '.'), f, chalk.dim(Fonts.getFontStyles(this.data.fonts[el.type], f))]); }); + if ( el.type != 'local' ) reco = true; }); + if ( reco ) this.recommendations.add('Third party', 'fonts'); } else this.ui.info('There were no Fonts found.'); this.remove('fonts'); @@ -392,6 +404,7 @@ class Tasks { this.data.ssl.constructor === Object) { this.ui.headline('SSL Certificate'); this.ui.error('There is no SSL/TLS available.', false); + this.recommendations.add('Security', 'available SSL certificate (it is mandatory that data is transmitted loud & clear)'); this.remove('ssl'); return; } else { @@ -425,6 +438,8 @@ class Tasks { this.args.cookies = parseInt(this.args.cookies,10) ? parseInt(this.args.cookies,10) : 13; + // TODO: ALL HEADERS including CSS & JS console.error('JS headers', this.data.js[0].headers); + // go away if no cookie is given if (typeof this.data.html.headers['set-cookie'] != 'object') { return; @@ -442,18 +457,23 @@ class Tasks { var strtotime = require('locutus/php/datetime/strtotime'); this.ui.headline('Cookies'); + var reco = false; + for ( var i in cookies ) { var t1 = cookies[i].expires.getTime(), t2 = new Date(strtotime('+'+this.args.cookies+' month 1 hour')*1000).getTime(); - + if ( t1 > t2 ) { - this.recommendations.add('Cookies', 'lifetime'); - this.ui.error('Cookie "'+cookies[i].name+'" expires in more than '+this.args.cookies+' month (expires on '+cookies[i].expires.toLocaleDateString()+')', true); + this.ui.error('Cookie "'+cookies[i].name+'" expires in more than '+this.args.cookies+' month (expires on '+cookies[i].expires.toLocaleDateString()+')', false); + reco = true; continue; } + this.ui.listitem(cookies[i].name, 'Expires on '+cookies[i].expires.toLocaleDateString()+' and '+(cookies[i].secure ? 'secure' : 'unsecure')); } + if ( reco ) this.recommendations.add('Cookies', 'expiration delay'); + this.remove('cookies'); return; } @@ -463,10 +483,7 @@ class Tasks { * @class Tasks */ getAuditInformations() { - if (!this.hasTask('audit')) { - console.error('no recommendation expected'); - return; - } + if (!this.hasTask('audit')) return; this.ui.headline('Recommendations for further human audit'); From e841da62bcd1bcd1d0e442f5efa9727275da4fd1 Mon Sep 17 00:00:00 2001 From: Baptiste LARVOL-SIMON Date: Fri, 8 Feb 2019 11:32:56 +0100 Subject: [PATCH 08/11] Add support of cookies set by CSS or JS resources It still lacks and other resources (videos & others) --- lib/cookies.js | 15 +++++++-------- lib/tasks.js | 23 +++++++++++++++++------ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/cookies.js b/lib/cookies.js index af96f49..cbb1201 100644 --- a/lib/cookies.js +++ b/lib/cookies.js @@ -28,28 +28,27 @@ var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; var sameSiteRegExp = /^(?:lax|strict)$/i -function Cookies(response){ - this.response = response; +function Cookies(cookies){ + this.raw_cookies = cookies; } /** * @return Array **/ Cookies.prototype.fetchAll = function() { - var header, cookies = []; + var cookies = []; - header = this.response.headers["set-cookie"]; - if (!header) return []; + if (!this.raw_cookies) return []; - header.forEach(cook => { + this.raw_cookies.forEach(cook => { var name, attrs = [], data = cook.split(/\\{0}; /) name = data.shift().split('='); data.forEach(attr => { - var dat = attr.split('='); - attrs[dat[0]] = dat[1]; + var dat = attr.split('='); + attrs[dat[0]] = dat[1]; }); cookies.push(new Cookie(name[0], name[1], attrs)); diff --git a/lib/tasks.js b/lib/tasks.js index 0c7452e..d7e37ba 100644 --- a/lib/tasks.js +++ b/lib/tasks.js @@ -437,15 +437,26 @@ class Tasks { } this.args.cookies = parseInt(this.args.cookies,10) ? parseInt(this.args.cookies,10) : 13; + var cookies = []; - // TODO: ALL HEADERS including CSS & JS console.error('JS headers', this.data.js[0].headers); + // direct cookies + if (typeof this.data.html.headers['set-cookie'] == 'object') { + cookies.push(this.data.html.headers['set-cookie']); + } + + // indirect cookies + [this.data.js, this.data.css].forEach(type => { + type.forEach(data => { + if (typeof data.headers['set-cookie'] == 'object') { + cookies.push(data.headers['set-cookie']); + } + }); + }); // go away if no cookie is given - if (typeof this.data.html.headers['set-cookie'] != 'object') { - return; - } + if ( cookies.length == 0 ) return; - var getter = new Cookies(this.data.html); + var getter = new Cookies(cookies); var cookies = getter.fetchAll(); if ( cookies.length == 0 ) { @@ -455,7 +466,7 @@ class Tasks { } var strtotime = require('locutus/php/datetime/strtotime'); - + this.ui.headline('Cookies'); var reco = false; From c36a16dcdf5201b9fa19fb466dbcc81150e9bc4d Mon Sep 17 00:00:00 2001 From: Baptiste LARVOL-SIMON Date: Mon, 11 Feb 2019 10:50:53 +0100 Subject: [PATCH 09/11] Add a better cookies support, including fonts and analytics headers --- lib/cookies.js | 26 +++++---- lib/fonts-parser.js | 7 ++- lib/tasks.js | 127 +++++++++++++++++++++++++++++++++----------- 3 files changed, 119 insertions(+), 41 deletions(-) diff --git a/lib/cookies.js b/lib/cookies.js index cbb1201..6e6d18d 100644 --- a/lib/cookies.js +++ b/lib/cookies.js @@ -28,6 +28,9 @@ var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; var sameSiteRegExp = /^(?:lax|strict)$/i +/** + * @param cookies array of arrays of cookies + */ function Cookies(cookies){ this.raw_cookies = cookies; } @@ -40,18 +43,21 @@ Cookies.prototype.fetchAll = function() { if (!this.raw_cookies) return []; - this.raw_cookies.forEach(cook => { - var name, - attrs = [], - data = cook.split(/\\{0}; /) + this.raw_cookies.forEach(cooks => { + cooks.forEach(cook => { + var data = cook.split(/\\{0}; /), + name, + attrs = [], + + name = data.shift().split('='); + + data.forEach(attr => { + var dat = attr.split('='); + attrs[dat[0]] = dat[1]; + }); - name = data.shift().split('='); - data.forEach(attr => { - var dat = attr.split('='); - attrs[dat[0]] = dat[1]; + cookies.push(new Cookie(name[0], name[1], attrs)); }); - - cookies.push(new Cookie(name[0], name[1], attrs)); }); return cookies; diff --git a/lib/fonts-parser.js b/lib/fonts-parser.js index 8b3eb1d..2db284e 100644 --- a/lib/fonts-parser.js +++ b/lib/fonts-parser.js @@ -53,6 +53,7 @@ class FontsParser { if (this.getFontStyle(ff) !== '') font.style = this.getFontStyle(ff); if (this.getFontWeight(ff) !== '') font.weight = this.getFontWeight(ff); font.type = this.getFontType(ff, this.localUrl); + font.url = el.url; fonts.push(font); }); @@ -94,6 +95,7 @@ class FontsParser { if (this.getFontStyle(ff) !== '') font.style = this.getFontStyle(ff); if (this.getFontWeight(ff) !== '') font.weight = this.getFontWeight(ff); font.type = 'local'; + font.url = el.url; fonts.push(font); } }); @@ -110,6 +112,7 @@ class FontsParser { if (this.getFontStyle(ff) !== '') font.style = this.getFontStyle(ff); if (this.getFontWeight(ff) !== '') font.weight = this.getFontWeight(ff); font.type = 'local'; + font.url = el.url; fonts.push(font); } }); @@ -170,6 +173,7 @@ class FontsParser { if (this.getFontStyle(ff) !== '') font.style = this.getFontStyle(ff); if (this.getFontWeight(ff) !== '') font.weight = this.getFontWeight(ff); font.type = 'google'; + font.url = el.url; fonts.push(font); }); } @@ -198,6 +202,7 @@ class FontsParser { if (this.getFontStyle(ff) !== '') font.style = this.getFontStyle(ff); if (this.getFontWeight(ff) !== '') font.weight = this.getFontWeight(ff); font.type = 'typekit'; + font.url = el.url; fonts.push(font); }); } @@ -383,4 +388,4 @@ class FontsParser { } -module.exports = FontsParser; \ No newline at end of file +module.exports = FontsParser; diff --git a/lib/tasks.js b/lib/tasks.js index d7e37ba..3de0959 100644 --- a/lib/tasks.js +++ b/lib/tasks.js @@ -92,7 +92,7 @@ class Tasks { /** - * Revoves a Task from the list + * Removes a Task from the list * @param {string} task * @class Tasks */ @@ -103,6 +103,18 @@ class Tasks { } + /** + * Removes a Task from the list and launch ending actions + * @param {string} task + * @class Tasks + */ + end(task) { + this.remove(task); + if ( this.tasks.length > 1 ) return; + this.getAuditInformations(); + } + + /** * Checks if the task list has a specifig task * @param {string} task @@ -172,7 +184,6 @@ class Tasks { this.getAnalyticsInformation(); this.getCDNInformation(); this.getCookiesInformation(); - this.getAuditInformations(); console.log(''); // console.log('Remaining: ', this.tasks); @@ -194,16 +205,18 @@ class Tasks { this.data.social.fb_graph = social.hasFacebookSocialGraph(this.data.js); this.data.social.pinterest = social.hasPinterest(this.data.js); + this.ui.headline('Social Medias'); + for ( var sm in this.data.social ) { if ( !this.data.social[sm] ) continue; this.recommendations.add('Social medias', sm); } - - //console.log('FB CONNECT:', this.data.social.fb_connect); - //console.log('FB SOCIAL GRAPH:', this.data.social.fb_graph); - //console.log('PINTEREST:', this.data.social.pinterest); - this.remove('social'); + if ( this.data.social.fb_connect ) this.ui.listitem('Facebook', 'Connect'); + if ( this.data.social.fb_graph ) this.ui.listitem('Facebook', 'Graph'); + if ( this.data.social.pinterest ) this.ui.listitem('Pinterest', ''); + + this.end('social'); } } @@ -241,7 +254,7 @@ class Tasks { this.recommendations.add('Third Party', 'Content Delivery Networks'); } else this.ui.info('No Content Delivery Networks have been found.'); - this.remove('cdn'); + this.end('cdn'); } } @@ -259,7 +272,7 @@ class Tasks { if (typeof this.data.analytics.ga === 'undefined' && typeof this.data.analytics.gtag === 'undefined' && typeof this.data.analytics.piwik === 'undefined' && typeof this.data.analytics.wordpress === 'undefined') { this.ui.info('No Analytics Tool has been found.'); - this.remove('analytics'); + this.end('analytics'); return; } @@ -289,7 +302,7 @@ class Tasks { } this.recommendations.add('Third party', 'analytics - web statistics'); - this.remove('analytics'); + this.end('analytics'); } } @@ -303,7 +316,7 @@ class Tasks { this.ui.info( chalk.yellow(this.data.html.url.hostname) + ' supports no DNS prefetching.'); - this.remove('prefetching'); + this.end('prefetching'); return; } @@ -337,7 +350,7 @@ class Tasks { }); this.recommendations.add('Third party', 'DNS Prefetching'); - this.remove('prefetching'); + this.end('prefetching'); } } @@ -363,7 +376,6 @@ class Tasks { this.data.fonts = {}; const Fonts = new FontsParser(this.data.css, this.data.js, this.data.html.url); this.data.fonts = Fonts.getFonts(); - //console.log(this.data.fonts); let todo = []; default_types.forEach(el => { @@ -390,7 +402,7 @@ class Tasks { if ( reco ) this.recommendations.add('Third party', 'fonts'); } else this.ui.info('There were no Fonts found.'); - this.remove('fonts'); + this.end('fonts'); } } @@ -405,7 +417,7 @@ class Tasks { this.ui.headline('SSL Certificate'); this.ui.error('There is no SSL/TLS available.', false); this.recommendations.add('Security', 'available SSL certificate (it is mandatory that data is transmitted loud & clear)'); - this.remove('ssl'); + this.end('ssl'); return; } else { this.ui.headline('SSL Certificate'); @@ -421,7 +433,7 @@ class Tasks { this.ui.listitem('FP SHA-1', this.data.ssl.fingerprint); this.ui.listitem('FP SHA-256', this.data.ssl.fingerprint256); //this.ui.message('SSL FINISHED'); - this.remove('ssl'); + this.end('ssl'); } } } @@ -436,32 +448,72 @@ class Tasks { return; } + var raw_cookies = [], + waitfor = []; + this.args.cookies = parseInt(this.args.cookies,10) ? parseInt(this.args.cookies,10) : 13; - var cookies = []; // direct cookies if (typeof this.data.html.headers['set-cookie'] == 'object') { - cookies.push(this.data.html.headers['set-cookie']); + raw_cookies.push(this.data.html.headers['set-cookie']); } - // indirect cookies + // process JS & CSS [this.data.js, this.data.css].forEach(type => { type.forEach(data => { if (typeof data.headers['set-cookie'] == 'object') { - cookies.push(data.headers['set-cookie']); + raw_cookies.push(data.headers['set-cookie']); } }); }); + // process analytics + if ( this.data.analytics ) { + for ( var i in this.data.analytics ) { + waitfor.push('analytics'); + this.loadExternalItem(this.data.analytics[i]).then(res => { + if (typeof res.headers['set-cookie'] == 'object') { + raw_cookies.push(res.headers['set-cookie']); + } + waitfor.splice(waitfor.indexOf('analytics'), 1); + this._innerCookies(raw_cookies, waitfor); + }); + } + } + + // process fonts + if ( this.data.fonts ) { + for ( var i in this.data.fonts ) { + if ( i == 'local' ) continue; + waitfor.push('fonts'); + this.data.fonts[i].forEach(font => { + this.loadExternalItem(font).then(res => { + if (typeof res.headers['set-cookie'] == 'object') { + raw_cookies.push(res.headers['set-cookie']); + } + waitfor.splice(waitfor.indexOf('fonts'), 1); + this._innerCookies(raw_cookies, waitfor); + }); + }); + } + } + + this._innerCookies(raw_cookies, waitfor); + } + + _innerCookies(raw_cookies, waitfor) { + // go away if still waiting for async calls + if ( waitfor.length > 0 ) return; + // go away if no cookie is given - if ( cookies.length == 0 ) return; + if ( raw_cookies.length == 0 ) return; - var getter = new Cookies(cookies); + var getter = new Cookies(raw_cookies); var cookies = getter.fetchAll(); if ( cookies.length == 0 ) { this.ui.headline('No cookie set'); - this.remove('cookies'); + this.end('cookies'); return; } @@ -471,11 +523,16 @@ class Tasks { var reco = false; for ( var i in cookies ) { + if ( cookies[i].expires == undefined ) { + this.ui.listitem(cookies[i].name, 'Expires at the end of the user session and '+(cookies[i].secure ? 'secure' : 'unsecure')); + continue; + } + var t1 = cookies[i].expires.getTime(), t2 = new Date(strtotime('+'+this.args.cookies+' month 1 hour')*1000).getTime(); if ( t1 > t2 ) { - this.ui.error('Cookie "'+cookies[i].name+'" expires in more than '+this.args.cookies+' month (expires on '+cookies[i].expires.toLocaleDateString()+')', false); + this.ui.listitem(chalk.red(cookies[i].name), 'expires in more than '+this.args.cookies+' month (expires on '+cookies[i].expires.toLocaleDateString()+')', false); reco = true; continue; } @@ -485,7 +542,7 @@ class Tasks { if ( reco ) this.recommendations.add('Cookies', 'expiration delay'); - this.remove('cookies'); + this.end('cookies'); return; } @@ -530,7 +587,7 @@ class Tasks { if (typeof this.data.general.plugins !== 'undefined' && this.data.general.plugins.length > 0) this.ui.listitem('Plugins', this.data.general.plugins.join(', ')); } - this.remove('general'); + this.end('general'); } } @@ -562,7 +619,7 @@ class Tasks { // console.log(this.data.js); } // this.ui.message('ADDITIONAL CONTENT FINISHED'); - this.remove('html'); + this.end('html'); return Promise.resolve(this.urls); } else return Promise.resolve(100); // Status 100 CONTINUE @@ -631,7 +688,7 @@ class Tasks { }, false); this.data.js.inline = this.hp.getInlineJS(); // this.ui.message('JS FINISHED'); - this.remove('js'); + this.end('js'); return res; }); } else @@ -660,7 +717,7 @@ class Tasks { }, false); this.data.css.inline = this.hp.getInlineCSS(); // this.ui.message('CSS FINISHED'); - this.remove('css'); + this.end('css'); return res; }); } else @@ -727,9 +784,19 @@ class Tasks { * @param {URL} item */ loadExternalItem(item) { + var tasks = this; + return new Promise(resolve => { const Obj = {}; Obj.url = item.url; + + // manage '^//' URLs adding the default protocol as a prefix + if ( typeof item.url == 'string' && + item.url.replace(/^(\/\/).*$/, '$1') == '//' && + tasks.data.html.url !== undefined ) { + item.url = tasks.data.html.url.protocol + item.url; + } + got(item.url).then( res => { Obj.headers = res.headers; @@ -776,7 +843,7 @@ class Tasks { chalk.yellow(this.data.html.url.href) + '.' }); } - this.remove('normalize'); + this.end('normalize'); Promise.resolve(200); }); } From aade92ac3cc86857491e002750d2114aae633d6b Mon Sep 17 00:00:00 2001 From: Baptiste LARVOL-SIMON Date: Mon, 11 Feb 2019 11:00:48 +0100 Subject: [PATCH 10/11] Nfs67147 (#1) * add preliminary changes * Going further into NF Z67-147 + preparation of further auditing * rollback to NF Z67-147 only * Add informations about viruses * Correct async information print out * Add user-agent --- index.js | 4 +++- lib/tasks.js | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + test.js | 29 +++++++++++++++++++++++ 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 test.js diff --git a/index.js b/index.js index b6f5385..b07a9cd 100755 --- a/index.js +++ b/index.js @@ -13,7 +13,8 @@ const ui = new UI(); // initialize the UI app .version(require('./package.json').version, '-V, --version') .option('-v, --verbose', 'shows you every single step') - .option('-m, --mute', 'shows only the results of the analysis'); + .option('-z, --nfz', 'displays informations related to website report operating procedure, AKA NF Z67-147 in France.') + .option('-m, --mute', 'shows only the results of the analysis') app .command('scan [url]') @@ -49,6 +50,7 @@ app // initialize the task runner const tasks = new Tasks(url, ui, args); + if (args.parent.nfz) tasks.new('nfz'); if (args.ssl) tasks.new('ssl'); if (args.cookies) tasks.new('cookies'); if (args.fonts) tasks.new('fonts'); diff --git a/lib/tasks.js b/lib/tasks.js index 9705a9d..a26227f 100644 --- a/lib/tasks.js +++ b/lib/tasks.js @@ -30,6 +30,9 @@ class Tasks { dependencies: ['html', 'css', 'js'], mandatory: true }, + nfz: { + dependencies: [], + }, cookies: { dependencies: ['html', 'css'] }, @@ -160,6 +163,7 @@ class Tasks { // console.log('Remaining: ', this.tasks); this.getGeneralInformation(); + this.getNFZInformations(); this.getSSLInformation(); this.getFontInformation(); this.getSocialMediaInformation(); @@ -188,6 +192,10 @@ class Tasks { this.data.social.fb_graph = social.hasFacebookSocialGraph(this.data.js); this.data.social.pinterest = social.hasPinterest(this.data.js); + for ( var sm in this.data.social ) { + if ( !this.data.social[sm] ) continue; + } + //console.log('FB CONNECT:', this.data.social.fb_connect); //console.log('FB SOCIAL GRAPH:', this.data.social.fb_graph); //console.log('PINTEREST:', this.data.social.pinterest); @@ -447,6 +455,63 @@ class Tasks { } + /** + * Gathes NF Z67-147 informations (meta data) about the audit + * @class Tasks + */ + getNFZInformations() { + if (!this.hasTask('nfz')) return; + + const dns = require('dns'); + const tld = this.url.replace(/^https{0,1}:\/\/([^\/]+)\/.*$/, '$1'); + var tasks = this; + + var w4, w6, a4, a6; + + dns.resolve(tld, 'A', function(err, addresses){ + w4 = err !== null ? null : addresses; + tasks._innerNFZInformations(w4, w6, a4, a6); + }); + dns.resolve(tld, 'AAAA', function(err, addresses){ + w6 = err !== null ? null : addresses; + tasks._innerNFZInformations(w4, w6, a4, a6); + }); + dns.resolve('resolver1.opendns.com', 'A', function(err, addresses){ + dns.setServers(addresses); + dns.resolve('myip.opendns.com', 'A', function(err, addresses){ + a4 = err !== null ? null : addresses; + tasks._innerNFZInformations(w4, w6, a4, a6); + }); + dns.resolve('myip.opendns.com', 'AAAA', function(err, addresses){ + a6 = err !== null ? null : addresses; + tasks._innerNFZInformations(w4, w6, a4, a6); + }); + }); + } + + _innerNFZInformations(w4, w6, a4, a6) { + if ( w4 === undefined || w6 === undefined || a4 === undefined || a6 === undefined ) return; + + const os = require('os') + const pkg = require('../package.json'); + + this.ui.headline('NF Z67-147 informations about the current audit'); + + this.ui.listitem('Operating system', os.type()+' '+os.release()+' '+os.arch()); + this.ui.listitem('Software', pkg.name+'-'+pkg.version+' - '+pkg.description); + this.ui.listitem('User Agent', this.default_headers['user-agent']); + this.ui.listitem('Web cache', 'Empty'); + this.ui.listitem('Web cookies', 'Empty'); + this.ui.listitem('Web proxy', 'Null'); + this.ui.listitem('Viruses', os.type() == 'Linux' ? 'Unix system up-to-date and not corrupted' : 'Unverified'); + this.ui.listitem('Date & time', new Date().toLocaleString()); + + if ( a4 !== null ) this.ui.listitem("Auditor's IPv4", a4); + if ( a6 !== null ) this.ui.listitem("Auditor's IPv6", a6); + if ( w4 !== null ) this.ui.listitem("Website IPv4", w4); + if ( w6 !== null ) this.ui.listitem("Website IPv6", w6); + } + /** * Gathes General Information (meta data) of the Website * @class Tasks @@ -723,3 +788,4 @@ class Tasks { } module.exports = Tasks; + diff --git a/package.json b/package.json index 48987fd..af0b1dd 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "repository": "https://github.com/mirkoschubert/gdpr-check.git", "author": "Mirko Schubert ", + "contributors": ["Baptiste LARVOL-SIMON (http://www.e-glop.net/)"], "license": "MIT", "dependencies": { "chalk": "^2.4.1", diff --git a/test.js b/test.js new file mode 100644 index 0000000..29e4b34 --- /dev/null +++ b/test.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node --harmony + +'use strict' + +const arr = []; +console.log(typeof arr); + +/* +const os = require('os'); +const pkg = require('./package.json'); + +console.error(os.type(), os.release(), os.arch()); +console.error(pkg.name, pkg.version, pkg.description); +console.error(new Date().toLocaleString()); + +const dns = require('dns'); +const url = 'http://myurl.tld/glop'; +console.error(url.replace(/^https{0,1}:\/\/([^\/]+)\/.*$/, '$1')); +//dns.resolve('resolver1.opendns.com', 'A', function(err, addresses){ + +/* +dns.resolve('resolver1.opendns.com', 'A', function(err, addresses){ + console.error(err, addresses); + dns.setServers(addresses); + dns.resolve('myip.opendns.com', 'A', function(err, addresses){ + console.error(err, addresses); + }); +}) +*/ From b5e6467f0940869e292d7e058a9d43e467027d9e Mon Sep 17 00:00:00 2001 From: Baptiste LARVOL-SIMON Date: Mon, 11 Feb 2019 11:02:49 +0100 Subject: [PATCH 11/11] Recommendations (#2) * add preliminary changes * Going further into NF Z67-147 + preparation of further auditing * rollback to NF Z67-147 only * Add conclusions as recommendations for further human auditing * Remove any NFZ67-147 reference to harden the ability to merge with upstream * done --- index.js | 2 + lib/recommendation-collection.js | 39 +++++++++++ lib/tasks.js | 113 +++++++++++++++++++++---------- 3 files changed, 119 insertions(+), 35 deletions(-) create mode 100644 lib/recommendation-collection.js diff --git a/index.js b/index.js index b07a9cd..6f226d0 100755 --- a/index.js +++ b/index.js @@ -13,6 +13,7 @@ const ui = new UI(); // initialize the UI app .version(require('./package.json').version, '-V, --version') .option('-v, --verbose', 'shows you every single step') + .option('-u, --audit', 'displays recommandations for further auditing') .option('-z, --nfz', 'displays informations related to website report operating procedure, AKA NF Z67-147 in France.') .option('-m, --mute', 'shows only the results of the analysis') @@ -50,6 +51,7 @@ app // initialize the task runner const tasks = new Tasks(url, ui, args); + if (args.parent.audit) tasks.new('audit'); if (args.parent.nfz) tasks.new('nfz'); if (args.ssl) tasks.new('ssl'); if (args.cookies) tasks.new('cookies'); diff --git a/lib/recommendation-collection.js b/lib/recommendation-collection.js new file mode 100644 index 0000000..ed8cd05 --- /dev/null +++ b/lib/recommendation-collection.js @@ -0,0 +1,39 @@ +/* + * recommendation-collection + * Copyright(c) 2018 Baptiste LARVOL-SIMON, http://www.e-glop.net/ + * MIT Licensed + */ + +'use strict' + +function RecommendationCollection(){ +} + +RecommendationCollection.prototype.collection = {}; + +/** + * @return void + **/ +RecommendationCollection.prototype.add = function(key, value) { + if ( !Array.isArray(this.collection[key]) ) { + this.collection[key] = [] + } + + this.collection[key].push(value); +} + +/** + * @return Array + **/ +RecommendationCollection.prototype.getTopics = function() { + return Object.keys(this.collection); +} + +/** + * @return Array + **/ +RecommendationCollection.prototype.getWarningsFor = function(key) { + return this.collection[key]; +} + +module.exports = RecommendationCollection; diff --git a/lib/tasks.js b/lib/tasks.js index a26227f..87fd246 100644 --- a/lib/tasks.js +++ b/lib/tasks.js @@ -9,6 +9,7 @@ const FontsParser = require('./fonts-parser'); // const tools = require('./tools'); const UI = require('./ui'); const Cookies = require('./cookies'); +const RecommendationCollection = require('./recommendation-collection'); class Tasks { constructor(url, uiInstance, args) { @@ -30,6 +31,7 @@ class Tasks { dependencies: ['html', 'css', 'js'], mandatory: true }, + audit: { nfz: { dependencies: [], }, @@ -68,6 +70,7 @@ class Tasks { this.url = url; this.tasks = []; this.data = {}; + this.recommendations = new RecommendationCollection(); // Put mandatory tasks already in the task list this.tasks = this.tasks.concat(this.getMandatoryTasks()); @@ -171,6 +174,7 @@ class Tasks { this.getAnalyticsInformation(); this.getCDNInformation(); this.getCookiesInformation(); + this.getAuditInformations(); console.log(''); // console.log('Remaining: ', this.tasks); @@ -194,13 +198,13 @@ class Tasks { for ( var sm in this.data.social ) { if ( !this.data.social[sm] ) continue; + this.recommendations.add('Social medias', sm); } //console.log('FB CONNECT:', this.data.social.fb_connect); //console.log('FB SOCIAL GRAPH:', this.data.social.fb_graph); //console.log('PINTEREST:', this.data.social.pinterest); - this.remove('social'); } } @@ -233,9 +237,12 @@ class Tasks { this.ui.info(chalk.yellow(this.data.html.url.hostname) + ' uses ' + chalk.red(found.length) + ' Content Delivery Networks.\n'); found.forEach((cdn, i) => { this.ui.tableitem( - [chalk.yellow(String(i + 1) + '.'), cdn.host, chalk.dim(cdn.description)]); + [chalk.yellow(String(i + 1) + '.'), cdn.host, chalk.dim(cdn.description)] + ); }); + this.recommendations.add('Third Party', 'Content Delivery Networks'); } else this.ui.info('No Content Delivery Networks have been found.'); + this.remove('cdn'); } } @@ -252,8 +259,11 @@ class Tasks { this.ui.headline('Analytics'); if (typeof this.data.analytics.ga === 'undefined' && typeof this.data.analytics.gtag === 'undefined' && - typeof this.data.analytics.piwik === 'undefined' && typeof this.data.analytics.wordpress === 'undefined') + typeof this.data.analytics.piwik === 'undefined' && typeof this.data.analytics.wordpress === 'undefined') { this.ui.info('No Analytics Tool has been found.'); + this.remove('analytics'); + return; + } if (typeof this.data.analytics.ga !== 'undefined') { this.ui.info(chalk.red('Google Analytics') + ' has been found.\n'); @@ -280,6 +290,7 @@ class Tasks { ' (Jetpack) has been found.'); } + this.recommendations.add('Third party', 'analytics - web statistics'); this.remove('analytics'); } } @@ -293,37 +304,40 @@ class Tasks { if (dns.length === 0) { this.ui.info( chalk.yellow(this.data.html.url.hostname) + - ' supports no DNS prefetching.') - } else { - this.ui.info( - chalk.yellow(this.data.html.url.hostname) + ' has ' + - chalk.red(dns.length) + ' DNS prefetching elements.\n'); - this.ui.settable({ - cells: 3, - widths: [5, 30, 0] - }); - - dns.forEach((url, i) => { - let u = (url.startsWith('//')) ? url.replace('//', '') : url; - let explanation = ''; - - if (u.indexOf('fonts.googleapis.com') >= 0) explanation = 'Google Fonts'; - if (u.indexOf('gravatar.com') >= 0) explanation = 'Automattic Gravatar Service'; - if (u.indexOf('s.w.org') >= 0) explanation = 'WordPress Emojis CDN'; - if (u.match(/s[0-9]\.wp\.com/)) explanation = 'WordPress Styles CDN'; - if (u.match(/i[0-9]\.wp\.com/)) explanation = 'WordPress Images CDN'; - if (u.match(/v[0-9]\.wordpress\.com/)) explanation = 'WordPress Videos CDN'; - if (u.indexOf('maxcdn.bootstrapcdn.com') >= 0) explanation = 'Bootstrap CDN'; - if (u.indexOf('checkout.stripe.com') >= 0) explanation = 'Stripe Online Payments'; - if (u.indexOf('code.jquery.com') >= 0) explanation = 'jQuery CDN'; - if (u.indexOf('translate.google.com') >= 0) explanation = 'Google Translate'; - if (u.indexOf('use.typekit.net') >= 0) explanation = 'Adobe Typekit Web Fonts'; - if (u.indexOf('use.fontawesome.com') >= 0) explanation = 'Font Awesome CDN'; - - this.ui.tableitem( - [chalk.yellow(String(i + 1) + '.'), u, chalk.dim(explanation)]) - }); + ' supports no DNS prefetching.'); + this.remove('prefetching'); + return; } + + this.ui.info( + chalk.yellow(this.data.html.url.hostname) + ' has ' + + chalk.red(dns.length) + ' DNS prefetching elements.\n'); + this.ui.settable({ + cells: 3, + widths: [5, 30, 0] + }); + + dns.forEach((url, i) => { + let u = (url.startsWith('//')) ? url.replace('//', '') : url; + let explanation = ''; + + if (u.indexOf('fonts.googleapis.com') >= 0) explanation = 'Google Fonts'; + if (u.indexOf('gravatar.com') >= 0) explanation = 'Automattic Gravatar Service'; + if (u.indexOf('s.w.org') >= 0) explanation = 'WordPress Emojis CDN'; + if (u.match(/s[0-9]\.wp\.com/)) explanation = 'WordPress Styles CDN'; + if (u.match(/i[0-9]\.wp\.com/)) explanation = 'WordPress Images CDN'; + if (u.match(/v[0-9]\.wordpress\.com/)) explanation = 'WordPress Videos CDN'; + if (u.indexOf('maxcdn.bootstrapcdn.com') >= 0) explanation = 'Bootstrap CDN'; + if (u.indexOf('checkout.stripe.com') >= 0) explanation = 'Stripe Online Payments'; + if (u.indexOf('code.jquery.com') >= 0) explanation = 'jQuery CDN'; + if (u.indexOf('translate.google.com') >= 0) explanation = 'Google Translate'; + if (u.indexOf('use.typekit.net') >= 0) explanation = 'Adobe Typekit Web Fonts'; + if (u.indexOf('use.fontawesome.com') >= 0) explanation = 'Font Awesome CDN'; + + this.ui.tableitem( + [chalk.yellow(String(i + 1) + '.'), u, chalk.dim(explanation)]) + }); + this.recommendations.add('Third party', 'DNS Prefetching'); this.remove('prefetching'); } @@ -365,6 +379,7 @@ class Tasks { }); if (todo.length !== 0) { + var reco = false; todo.forEach((el, i) => { let count = Fonts.countFonts(this.data.fonts[el.type]); if (i > 0) console.log(''); @@ -372,7 +387,9 @@ class Tasks { count.forEach((f, j) => { this.ui.tableitem([chalk.yellow(String(j + 1) + '.'), f, chalk.dim(Fonts.getFontStyles(this.data.fonts[el.type], f))]); }); + if ( el.type != 'local' ) reco = true; }); + if ( reco ) this.recommendations.add('Third party', 'fonts'); } else this.ui.info('There were no Fonts found.'); this.remove('fonts'); @@ -389,6 +406,7 @@ class Tasks { this.data.ssl.constructor === Object) { this.ui.headline('SSL Certificate'); this.ui.error('There is no SSL/TLS available.', false); + this.recommendations.add('Security', 'available SSL certificate (it is mandatory that data is transmitted loud & clear)'); this.remove('ssl'); return; } else { @@ -422,6 +440,8 @@ class Tasks { this.args.cookies = parseInt(this.args.cookies,10) ? parseInt(this.args.cookies,10) : 13; + // TODO: ALL HEADERS including CSS & JS console.error('JS headers', this.data.js[0].headers); + // go away if no cookie is given if (typeof this.data.html.headers['set-cookie'] != 'object') { return; @@ -439,22 +459,45 @@ class Tasks { var strtotime = require('locutus/php/datetime/strtotime'); this.ui.headline('Cookies'); + var reco = false; + for ( var i in cookies ) { var t1 = cookies[i].expires.getTime(), t2 = new Date(strtotime('+'+this.args.cookies+' month 1 hour')*1000).getTime(); - + if ( t1 > t2 ) { - this.ui.error('Cookie "'+cookies[i].name+'" expires in more than '+this.args.cookies+' month (expires on '+cookies[i].expires.toLocaleDateString()+')', true); + this.ui.error('Cookie "'+cookies[i].name+'" expires in more than '+this.args.cookies+' month (expires on '+cookies[i].expires.toLocaleDateString()+')', false); + reco = true; continue; } + this.ui.listitem(cookies[i].name, 'Expires on '+cookies[i].expires.toLocaleDateString()+' and '+(cookies[i].secure ? 'secure' : 'unsecure')); } + if ( reco ) this.recommendations.add('Cookies', 'expiration delay'); + this.remove('cookies'); return; } + /** + * Gathes further recommendations for further human audit + * @class Tasks + */ + getAuditInformations() { + if (!this.hasTask('audit')) return; + + this.ui.headline('Recommendations for further human audit'); + var topics = this.recommendations.getTopics(); + for ( var i in topics ) { + var recos = this.recommendations.getWarningsFor(topics[i]); + recos.forEach(reco => { + this.ui.listitem(topics[i], 'Check '+reco); + }); + } + } + /** * Gathes NF Z67-147 informations (meta data) about the audit * @class Tasks