auditforge / backend /src /lib /report-generator.js
Kaballas's picture
initialize project structure with essential configurations and components
56b6519
var fs = require('fs');
var Docxtemplater = require('docxtemplater');
var PizZip = require('pizzip');
var expressions = require('./report-filters');
var ImageModule = require('docxtemplater-image-module-pwndoc');
var sizeOf = require('image-size');
var customGenerator = require('./custom-generator');
var utils = require('./utils');
var _ = require('lodash');
var Image = require('mongoose').model('Image');
const libre = require('libreoffice-convert');
const { parseAsync } = require('json2csv');
var Settings = require('mongoose').model('Settings');
var CVSS31 = require('./cvsscalc31.js');
var translate = require('../translate');
var $t;
const muhammara = require('muhammara');
const path = require('path');
const os = require('os');
const { v4: uuidv4 } = require('uuid');
// Generate document with docxtemplater
async function generateDoc(audit) {
var templatePath = `${__basedir}/../report-templates/${audit.template.name}.${audit.template.ext || 'docx'}`;
var content = fs.readFileSync(templatePath, 'binary');
var zip = new PizZip(content);
translate.setLocale(audit.language);
$t = translate.translate;
var settings = await Settings.getAll();
var preppedAudit = await prepAuditData(audit, settings);
var opts = {};
// opts.centered = true;
opts.getImage = function (tagValue, tagName) {
if (tagValue !== 'undefined') {
tagValue = tagValue.split(',')[1];
return Buffer.from(tagValue, 'base64');
}
// return fs.readFileSync(tagValue, {encoding: 'base64'});
};
opts.getSize = function (img, tagValue, tagName) {
if (img) {
var sizeObj = sizeOf(img);
var width = sizeObj.width;
var height = sizeObj.height;
if (tagName === 'company.logo_small') {
var divider = sizeObj.height / 37;
height = 37;
width = Math.floor(sizeObj.width / divider);
} else if (tagName === 'company.logo') {
var divider = sizeObj.height / 250;
height = 250;
width = Math.floor(sizeObj.width / divider);
if (width > 400) {
divider = sizeObj.width / 400;
height = Math.floor(sizeObj.height / divider);
width = 400;
}
} else if (sizeObj.width > 600) {
var divider = sizeObj.width / 600;
width = 600;
height = Math.floor(sizeObj.height / divider);
}
return [width, height];
}
return [0, 0];
};
if (
settings.report.private.imageBorder &&
settings.report.private.imageBorderColor
)
opts.border = settings.report.private.imageBorderColor.replace('#', '');
try {
var imageModule = new ImageModule(opts);
} catch (err) {
console.log(err);
}
var doc = new Docxtemplater()
.attachModule(imageModule)
.loadZip(zip)
.setOptions({ parser: parser, paragraphLoop: true });
customGenerator.apply(preppedAudit);
doc.setData(preppedAudit);
try {
doc.render();
} catch (error) {
if (error.properties.id === 'multi_error') {
error.properties.errors.forEach(function (err) {
console.log(err);
});
} else console.log(error);
if (error.properties && error.properties.errors instanceof Array) {
const errorMessages = error.properties.errors
.map(function (error) {
return `Explanation: ${error.properties.explanation}\nScope: ${JSON.stringify(error.properties.scope).substring(0, 142)}...`;
})
.join('\n\n');
// errorMessages is a humanly readable message looking like this :
// 'The tag beginning with "foobar" is unopened'
throw `Template Error:\n${errorMessages}`;
} else {
throw error;
}
}
var buf = doc.getZip().generate({ type: 'nodebuffer' });
return buf;
}
exports.generateDoc = generateDoc;
// Generates a PDF from a docx using libreoffice-convert
// libreoffice-convert leverages libreoffice to convert office documents to different formats
// https://www.npmjs.com/package/libreoffice-convert
async function generatePdf(audit) {
var docxReport = await generateDoc(audit);
return new Promise((resolve, reject) =>
libre.convert(docxReport, '.pdf', undefined, (err, pdf) => {
if (err) console.log(err);
resolve(pdf);
}),
);
}
exports.generatePdf = generatePdf;
// Generates a encrypted PDF using libreoffice-convert
// and muhammara to encrypt it with a given password.
// https://www.npmjs.com/package/muhammara
async function generateEncryptedPdf(audit, password) {
// Genera el archivo DOCX
var docxReport = await generateDoc(audit);
return new Promise((resolve, reject) => {
libre.convert(docxReport, '.pdf', undefined, (err, pdf) => {
if (err) {
console.log(err);
return reject(err);
}
const tempPdfPath = path.join(
os.tmpdir(),
`documento_sin_contraseña_${uuidv4()}.pdf`,
);
fs.writeFileSync(tempPdfPath, pdf);
const protectedPdfPath = path.join(
os.tmpdir(),
`documento_protegido_${uuidv4()}.pdf`,
);
try {
const Recipe = muhammara.Recipe;
const pdfDoc = new Recipe(tempPdfPath, protectedPdfPath);
pdfDoc
.encrypt({
userPassword: password,
ownerPassword: password,
userProtectionFlag: 4,
})
.endPDF();
const protectedPdf = fs.readFileSync(protectedPdfPath);
fs.unlinkSync(tempPdfPath);
fs.unlinkSync(protectedPdfPath);
resolve(protectedPdf);
} catch (error) {
console.error('Error protecting PDF:', error);
reject(error);
}
});
});
}
exports.generateEncryptedPdf = generateEncryptedPdf;
// Generates a csv from the json data
// Leverages json2csv
// https://www.npmjs.com/package/json2csv
async function generateCsv(audit) {
return parseAsync(audit._doc);
}
exports.generateCsv = generateCsv;
// Filters helper: handles the use of preformated easilly translatable strings.
// Source: https://www.tutorialstonight.com/javascript-string-format.php
String.prototype.format = function () {
let args = arguments;
return this.replace(/{([0-9]+)}/g, function (match, index) {
return typeof args[index] == 'undefined' ? match : args[index];
});
};
// Compile all angular expressions
var angularParser = function (tag) {
expressions = { ...expressions, ...customGenerator.expressions };
if (tag === '.') {
return {
get: function (s) {
return s;
},
};
}
const expr = expressions.compile(
tag.replace(/(’|‘)/g, "'").replace(/(“|”)/g, '"'),
);
return {
get: function (scope, context) {
let obj = {};
const scopeList = context.scopeList;
const num = context.num;
for (let i = 0, len = num + 1; i < len; i++) {
obj = _.merge(obj, scopeList[i]);
}
return expr(scope, obj);
},
};
};
function parser(tag) {
// We write an exception to handle the tag "$pageBreakExceptLast"
if (tag === '$pageBreakExceptLast') {
return {
get(scope, context) {
const totalLength =
context.scopePathLength[context.scopePathLength.length - 1];
const index = context.scopePathItem[context.scopePathItem.length - 1];
const isLast = index === totalLength - 1;
if (!isLast) {
return '<w:p><w:r><w:br w:type="page"/></w:r></w:p>';
} else {
return '';
}
},
};
}
// We use the angularParser as the default fallback
// If you don't wish to use the angularParser,
// you can use the default parser as documented here:
// https://docxtemplater.readthedocs.io/en/latest/configuration.html#default-parser
return angularParser(tag);
}
function cvssStrToObject(cvss) {
var initialState = 'Not Defined';
var res = {
AV: initialState,
AC: initialState,
PR: initialState,
UI: initialState,
S: initialState,
C: initialState,
I: initialState,
A: initialState,
E: initialState,
RL: initialState,
RC: initialState,
CR: initialState,
IR: initialState,
AR: initialState,
MAV: initialState,
MAC: initialState,
MPR: initialState,
MUI: initialState,
MS: initialState,
MC: initialState,
MI: initialState,
MA: initialState,
};
if (cvss) {
var temp = cvss.split('/');
for (var i = 0; i < temp.length; i++) {
var elt = temp[i].split(':');
switch (elt[0]) {
case 'AV':
if (elt[1] === 'N') res.AV = 'Network';
else if (elt[1] === 'A') res.AV = 'Adjacent Network';
else if (elt[1] === 'L') res.AV = 'Local';
else if (elt[1] === 'P') res.AV = 'Physical';
res.AV = $t(res.AV);
break;
case 'AC':
if (elt[1] === 'L') res.AC = 'Low';
else if (elt[1] === 'H') res.AC = 'High';
res.AC = $t(res.AC);
break;
case 'PR':
if (elt[1] === 'N') res.PR = 'None';
else if (elt[1] === 'L') res.PR = 'Low';
else if (elt[1] === 'H') res.PR = 'High';
res.PR = $t(res.PR);
break;
case 'UI':
if (elt[1] === 'N') res.UI = 'None';
else if (elt[1] === 'R') res.UI = 'Required';
res.UI = $t(res.UI);
break;
case 'S':
if (elt[1] === 'U') res.S = 'Unchanged';
else if (elt[1] === 'C') res.S = 'Changed';
res.S = $t(res.S);
break;
case 'C':
if (elt[1] === 'N') res.C = 'None';
else if (elt[1] === 'L') res.C = 'Low';
else if (elt[1] === 'H') res.C = 'High';
res.C = $t(res.C);
break;
case 'I':
if (elt[1] === 'N') res.I = 'None';
else if (elt[1] === 'L') res.I = 'Low';
else if (elt[1] === 'H') res.I = 'High';
res.I = $t(res.I);
break;
case 'A':
if (elt[1] === 'N') res.A = 'None';
else if (elt[1] === 'L') res.A = 'Low';
else if (elt[1] === 'H') res.A = 'High';
res.A = $t(res.A);
break;
case 'E':
if (elt[1] === 'U') res.E = 'Unproven';
else if (elt[1] === 'P') res.E = 'Proof-of-Concept';
else if (elt[1] === 'F') res.E = 'Functional';
else if (elt[1] === 'H') res.E = 'High';
res.E = $t(res.E);
break;
case 'RL':
if (elt[1] === 'O') res.RL = 'Official Fix';
else if (elt[1] === 'T') res.RL = 'Temporary Fix';
else if (elt[1] === 'W') res.RL = 'Workaround';
else if (elt[1] === 'U') res.RL = 'Unavailable';
res.RL = $t(res.RL);
break;
case 'RC':
if (elt[1] === 'U') res.RC = 'Unknown';
else if (elt[1] === 'R') res.RC = 'Reasonable';
else if (elt[1] === 'C') res.RC = 'Confirmed';
res.RC = $t(res.RC);
break;
case 'CR':
if (elt[1] === 'L') res.CR = 'Low';
else if (elt[1] === 'M') res.CR = 'Medium';
else if (elt[1] === 'H') res.CR = 'High';
res.CR = $t(res.CR);
break;
case 'IR':
if (elt[1] === 'L') res.IR = 'Low';
else if (elt[1] === 'M') res.IR = 'Medium';
else if (elt[1] === 'H') res.IR = 'High';
res.IR = $t(res.IR);
break;
case 'AR':
if (elt[1] === 'L') res.AR = 'Low';
else if (elt[1] === 'M') res.AR = 'Medium';
else if (elt[1] === 'H') res.AR = 'High';
res.AR = $t(res.AR);
break;
case 'MAV':
if (elt[1] === 'N') res.MAV = 'Network';
else if (elt[1] === 'A') res.MAV = 'Adjacent Network';
else if (elt[1] === 'L') res.MAV = 'Local';
else if (elt[1] === 'P') res.MAV = 'Physical';
res.MAV = $t(res.MAV);
break;
case 'MAC':
if (elt[1] === 'L') res.MAC = 'Low';
else if (elt[1] === 'H') res.MAC = 'High';
res.MAC = $t(res.MAC);
break;
case 'MPR':
if (elt[1] === 'N') res.MPR = 'None';
else if (elt[1] === 'L') res.MPR = 'Low';
else if (elt[1] === 'H') res.MPR = 'High';
res.MPR = $t(res.MPR);
break;
case 'MUI':
if (elt[1] === 'N') res.MUI = 'None';
else if (elt[1] === 'R') res.MUI = 'Required';
res.MUI = $t(res.MUI);
break;
case 'MS':
if (elt[1] === 'U') res.MS = 'Unchanged';
else if (elt[1] === 'C') res.MS = 'Changed';
res.MS = $t(res.MS);
break;
case 'MC':
if (elt[1] === 'N') res.MC = 'None';
else if (elt[1] === 'L') res.MC = 'Low';
else if (elt[1] === 'H') res.MC = 'High';
res.MC = $t(res.MC);
break;
case 'MI':
if (elt[1] === 'N') res.MI = 'None';
else if (elt[1] === 'L') res.MI = 'Low';
else if (elt[1] === 'H') res.MI = 'High';
res.MI = $t(res.MI);
break;
case 'MA':
if (elt[1] === 'N') res.MA = 'None';
else if (elt[1] === 'L') res.MA = 'Low';
else if (elt[1] === 'H') res.MA = 'High';
res.MA = $t(res.MA);
break;
default:
break;
}
}
}
return res;
}
async function prepAuditData(data, settings) {
/** CVSS Colors for table cells */
var noneColor = settings.report.public.cvssColors.noneColor.replace('#', ''); //default of blue ("#4A86E8")
var lowColor = settings.report.public.cvssColors.lowColor.replace('#', ''); //default of green ("#008000")
var mediumColor = settings.report.public.cvssColors.mediumColor.replace(
'#',
'',
); //default of yellow ("#f9a009")
var highColor = settings.report.public.cvssColors.highColor.replace('#', ''); //default of red ("#fe0000")
var criticalColor = settings.report.public.cvssColors.criticalColor.replace(
'#',
'',
); //default of black ("#212121")
var cellNoneColor =
'<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="' +
noneColor +
'"/></w:tcPr>';
var cellLowColor =
'<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="' +
lowColor +
'"/></w:tcPr>';
var cellMediumColor =
'<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="' +
mediumColor +
'"/></w:tcPr>';
var cellHighColor =
'<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="' +
highColor +
'"/></w:tcPr>';
var cellCriticalColor =
'<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="' +
criticalColor +
'"/></w:tcPr>';
var result = {};
result.name = data.name || 'undefined';
result.auditType = $t(data.auditType) || 'undefined';
result.date = data.date || 'undefined';
result.date_start = data.date_start || 'undefined';
result.date_end = data.date_end || 'undefined';
if (data.customFields) {
for (var field of data.customFields) {
var fieldType = field.customField.fieldType;
var label = field.customField.label;
if (fieldType === 'text')
result[_.deburr(label.toLowerCase()).replace(/\s/g, '')] =
await splitHTMLParagraphs(field.text);
else if (fieldType !== 'space')
result[_.deburr(label.toLowerCase()).replace(/\s/g, '')] = field.text;
}
}
result.company = {};
if (data.company) {
result.company.name = data.company.name || 'undefined';
result.company.shortName = data.company.shortName || result.company.name;
result.company.logo = data.company.logo || 'undefined';
result.company.logo_small = data.company.logo || 'undefined';
}
result.client = {};
if (data.client) {
result.client.email = data.client.email || 'undefined';
result.client.firstname = data.client.firstname || 'undefined';
result.client.lastname = data.client.lastname || 'undefined';
result.client.phone = data.client.phone || 'undefined';
result.client.cell = data.client.cell || 'undefined';
result.client.title = data.client.title || 'undefined';
}
result.collaborators = [];
data.collaborators.forEach(collab => {
result.collaborators.push({
username: collab.username || 'undefined',
firstname: collab.firstname || 'undefined',
lastname: collab.lastname || 'undefined',
email: collab.email || 'undefined',
phone: collab.phone || 'undefined',
role: collab.role || 'undefined',
});
});
result.language = data.language || 'undefined';
result.scope = data.scope.toObject() || [];
result.findings = [];
for (var finding of data.findings) {
var tmpCVSS = CVSS31.calculateCVSSFromVector(finding.cvssv3);
var tmpFinding = {
title: finding.title || '',
vulnType: $t(finding.vulnType) || '',
description: await splitHTMLParagraphs(finding.description),
observation: await splitHTMLParagraphs(finding.observation),
remediation: await splitHTMLParagraphs(finding.remediation),
remediationComplexity: finding.remediationComplexity || '',
priority: finding.priority || '',
references: finding.references || [],
cwes: finding.cwes || [],
poc: await splitHTMLParagraphs(finding.poc),
affected: finding.scope || '',
status: finding.status || '',
category: $t(finding.category) || $t('No Category'),
identifier: 'IDX-' + utils.lPad(finding.identifier),
retestStatus: finding?.retestStatus ? $t(finding.retestStatus) : '',
retestDescription: await splitHTMLParagraphs(finding.retestDescription),
};
// Handle CVSS
tmpFinding.cvss = {
vectorString: tmpCVSS.vectorString || '',
baseMetricScore: tmpCVSS.baseMetricScore || '',
baseSeverity: tmpCVSS.baseSeverity || '',
temporalMetricScore: tmpCVSS.temporalMetricScore || '',
temporalSeverity: tmpCVSS.temporalSeverity || '',
environmentalMetricScore: tmpCVSS.environmentalMetricScore || '',
environmentalSeverity: tmpCVSS.environmentalSeverity || '',
};
if (tmpCVSS.baseImpact)
tmpFinding.cvss.baseImpact = CVSS31.roundUp1(tmpCVSS.baseImpact);
else tmpFinding.cvss.baseImpact = '';
if (tmpCVSS.baseExploitability)
tmpFinding.cvss.baseExploitability = CVSS31.roundUp1(
tmpCVSS.baseExploitability,
);
else tmpFinding.cvss.baseExploitability = '';
if (tmpCVSS.environmentalModifiedImpact)
tmpFinding.cvss.environmentalModifiedImpact = CVSS31.roundUp1(
tmpCVSS.environmentalModifiedImpact,
);
else tmpFinding.cvss.environmentalModifiedImpact = '';
if (tmpCVSS.environmentalModifiedExploitability)
tmpFinding.cvss.environmentalModifiedExploitability = CVSS31.roundUp1(
tmpCVSS.environmentalModifiedExploitability,
);
else tmpFinding.cvss.environmentalModifiedExploitability = '';
if (tmpCVSS.baseSeverity === 'Low')
tmpFinding.cvss.cellColor = cellLowColor;
else if (tmpCVSS.baseSeverity === 'Medium')
tmpFinding.cvss.cellColor = cellMediumColor;
else if (tmpCVSS.baseSeverity === 'High')
tmpFinding.cvss.cellColor = cellHighColor;
else if (tmpCVSS.baseSeverity === 'Critical')
tmpFinding.cvss.cellColor = cellCriticalColor;
else tmpFinding.cvss.cellColor = cellNoneColor;
if (tmpCVSS.temporalSeverity === 'Low')
tmpFinding.cvss.temporalCellColor = cellLowColor;
else if (tmpCVSS.temporalSeverity === 'Medium')
tmpFinding.cvss.temporalCellColor = cellMediumColor;
else if (tmpCVSS.temporalSeverity === 'High')
tmpFinding.cvss.temporalCellColor = cellHighColor;
else if (tmpCVSS.temporalSeverity === 'Critical')
tmpFinding.cvss.temporalCellColor = cellCriticalColor;
else tmpFinding.cvss.temporalCellColor = cellNoneColor;
if (tmpCVSS.environmentalSeverity === 'Low')
tmpFinding.cvss.environmentalCellColor = cellLowColor;
else if (tmpCVSS.environmentalSeverity === 'Medium')
tmpFinding.cvss.environmentalCellColor = cellMediumColor;
else if (tmpCVSS.environmentalSeverity === 'High')
tmpFinding.cvss.environmentalCellColor = cellHighColor;
else if (tmpCVSS.environmentalSeverity === 'Critical')
tmpFinding.cvss.environmentalCellColor = cellCriticalColor;
else tmpFinding.cvss.environmentalCellColor = cellNoneColor;
tmpFinding.cvssObj = cvssStrToObject(tmpCVSS.vectorString);
if (finding.customFields) {
for (field of finding.customFields) {
// For retrocompatibility of findings with old customFields
// or if custom field has been deleted, last saved custom fields will be available
if (field.customField) {
var fieldType = field.customField.fieldType;
var label = field.customField.label;
} else {
var fieldType = field.fieldType;
var label = field.label;
}
if (fieldType === 'text')
tmpFinding[
_.deburr(label.toLowerCase())
.replace(/\s/g, '')
.replace(/[^\w]/g, '_')
] = await splitHTMLParagraphs(field.text);
else if (fieldType !== 'space')
tmpFinding[
_.deburr(label.toLowerCase())
.replace(/\s/g, '')
.replace(/[^\w]/g, '_')
] = field.text;
}
}
result.findings.push(tmpFinding);
}
result.categories = _.chain(result.findings)
.groupBy('category')
.map((value, key) => {
return { categoryName: key, categoryFindings: value };
})
.value();
result.creator = {};
if (data.creator) {
result.creator.username = data.creator.username || 'undefined';
result.creator.firstname = data.creator.firstname || 'undefined';
result.creator.lastname = data.creator.lastname || 'undefined';
result.creator.email = data.creator.email || 'undefined';
result.creator.phone = data.creator.phone || 'undefined';
result.creator.role = data.creator.role || 'undefined';
}
for (var section of data.sections) {
var formatSection = {
name: $t(section.name),
};
if (section.text)
// keep text for retrocompatibility
formatSection.text = await splitHTMLParagraphs(section.text);
else if (section.customFields) {
for (field of section.customFields) {
var fieldType = field.customField.fieldType;
var label = field.customField.label;
if (fieldType === 'text')
formatSection[
_.deburr(label.toLowerCase())
.replace(/\s/g, '')
.replace(/[^\w]/g, '_')
] = await splitHTMLParagraphs(field.text);
else if (fieldType !== 'space')
formatSection[
_.deburr(label.toLowerCase())
.replace(/\s/g, '')
.replace(/[^\w]/g, '_')
] = field.text;
}
}
result[section.field] = formatSection;
}
replaceSubTemplating(result);
return result;
}
async function splitHTMLParagraphs(data) {
var result = [];
if (!data) return result;
var splitted = data.split(/(<img.+?src=".*?".+?alt=".*?".*?>)/);
for (var value of splitted) {
if (value.startsWith('<img')) {
var src = value.match(/<img.+src="(.*?)"/) || '';
var alt = value.match(/<img.+alt="(.*?)"/) || '';
if (src && src.length > 1) src = src[1];
if (alt && alt.length > 1) alt = _.unescape(alt[1]);
if (!src.startsWith('data')) {
try {
src = (await Image.getOne(src)).value;
} catch (error) {
src = 'data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=';
}
}
if (result.length === 0) result.push({ text: '', images: [] });
result[result.length - 1].images.push({ image: src, caption: alt });
} else if (value === '') {
continue;
} else {
result.push({ text: value, images: [] });
}
}
return result;
}
function replaceSubTemplating(o, originalData = o) {
var regexp = /\{_\{([a-zA-Z0-9\[\]\_\.]{1,})\}_\}/gm;
if (Array.isArray(o))
o.forEach(key => replaceSubTemplating(key, originalData));
else if (typeof o === 'object' && !!o) {
Object.keys(o).forEach(key => {
if (typeof o[key] === 'string')
o[key] = o[key].replace(regexp, (match, word) =>
_.get(originalData, word.trim(), ''),
);
else replaceSubTemplating(o[key], originalData);
});
}
}