UNPKG

hackerone-report-formatter

Version:

Formats HackerOne report into more readable form

420 lines (355 loc) 9.89 kB
import moment from 'moment' import norm from './normalize' import { pick, omit, snakeCase } from 'lodash' export const normalize = norm export const formatDate = (date, unix = false) => { const momentDate = date ? moment(date, true) : null if (momentDate && momentDate.isValid()) { const utcDate = momentDate.utc() if (unix) { return utcDate.valueOf() } return utcDate.format('YYYY-MM-DDTHH:mm:ss.SSS\\Z') } return null } const getAvatar = (pictures = null, shortLinks = false) => { if (!pictures) { return null } const url = typeof pictures === 'object' ? pictures.medium || pictures.small : pictures if ( url.includes('rails/active_storage') || url.includes('assets/avatars/default-') || url.length > 500 ) { return null } return shortLinks ? url.split('variants')[1] : url } const format = ( data, { noAttachmentUrls = false, shortLinks = false, string = false, normalize = false, } = {}, ) => { if (!data) { return null } const { id, reporter, team: { handle, offers_bounties, default_currency, profile_picture_urls: program_pictures, only_cleared_hackers: only_verified_hackers = false, award_miles = false, profile, ...team }, vote_count, bounty_amount, weakness = null, disclosed_at, severity = null, severity_rating, title, activities, created_at, bug_reporter_agreed_on_going_public_at: h_agreed_at, team_agreed_on_going_public_at: p_agreed_at, attachments = [], original_report_id = null, original_report_url = null, structured_scope: asset, vulnerability_information, state, substate, summaries, visibility, cve_ids, } = typeof data === 'string' ? JSON.parse(data) : data const author_type = severity && severity.author_type ? severity.author_type : null const { cleared: verified, profile_picture_urls: hacker_pictures } = reporter || {} const username = reporter ? reporter.username : handle const awarded_to = [] let isIBB = false const hacker_picture_url = getAvatar(hacker_pictures, shortLinks) const program_picture_url = getAvatar(program_pictures, shortLinks) const programJson = { id: team.id, handle, picture_url: program_picture_url, ...(offers_bounties && { offers_bounties }), ...(award_miles && { award_miles }), ...(only_verified_hackers && { only_verified_hackers }), ...profile, } let json = { id, title, state: state.toLowerCase(), substate, platform: 'hackerone', hacker: reporter ? { verified, handle: username, picture_url: hacker_picture_url, } : null, program: programJson, original_report_id, original_report_url, vulnerable_asset: asset ? { type: asset.asset_type.toLowerCase(), identifier: asset.asset_identifier, max_severity: asset.max_severity, } : null, weakness, severity: severity ? { set_by_team: author_type && author_type.toLowerCase() === 'team', ...omit(severity, 'author_type'), } : null, award: null, vote_count, cve_ids: cve_ids, visibility, hacker_agreed_on_going_public_at: formatDate(h_agreed_at, true), platform_agreed_on_going_public_at: formatDate(p_agreed_at), created_at: formatDate(created_at, true), disclosed_at: formatDate(disclosed_at, true), // first_synced_at: moment().valueOf(), // last_synced_at: moment().valueOf(), vulnerability_information: vulnerability_information || null, summaries: summaries .filter(({ id, content }) => id && content) .map(({ id, content, category }) => ({ id, by: category == 'team' ? 'program' : 'hacker', content, })), // .reduce((result, summary) => { // return { // ...result, // [summary.by]: summary.content, // } // }, {}), attachments: attachments.map( ({ file_name: filename, expiring_url: url, ...att }) => ({ ...att, filename, ...(noAttachmentUrls === false && { url }), }), ), activities: activities.map(item => { const { created_at, updated_at, id, type, message, actor: actr, bounty_amount, bounty_currency: currency, bonus_amount, automated_response, collaborator, additional_data, first_to_agree, team_handle, email, attachments, reporter, cve_ids, } = item const { profile_picture_urls, ...actor } = actr || {} const action = snakeCase(type.split('::')[1]) const activity = { id, type: snakeCase(action), message: message || null, attachments: attachments && attachments.map(({ url, ...file }) => ({ ...file, ...(noAttachmentUrls === false && { url }), })), automated_response, updated_at: updated_at !== created_at ? formatDate(updated_at, true) : null, created_at: formatDate(created_at, true), actor: actor ? { handle: actor.username || team_handle, } : null, // item: actor, // raw: item, } if (!activity.updated_at) { delete activity.updated_at } if (actor.cleared) { activity.actor.verified = true } const actorAvatar = getAvatar(profile_picture_urls, shortLinks) if (actorAvatar) { activity.actor.picture_url = actorAvatar } if (actor.hackerone_triager) { activity.actor.platform_triager = true } if (actor.hackerone_employee) { activity.actor.platform_employee = true } const addData = data => { activity.additional_data = { ...(activity.additional_data || null), ...data, } } if (actor && actor.ibb) { activity.actor.ibb = true isIBB = true } if (reporter) { // activity.hacker = reporter } if (!automated_response) { delete activity.automated_response } if (!Array.isArray(attachments)) { delete activity.attachments } if (!message) { delete activity.message } if (automated_response) { activity.automated_response = true } if (additional_data) { const extraFields = Object.keys(additional_data).filter( field => /^(old|new|added|removed)_/.test(field) === false, ) if (extraFields.length > 0) { addData(pick(additional_data, extraFields)) } } if (email) { addData({ invited_hacker: email }) } if (action && action.includes('agreed_on_going_public')) { addData({ first_to_agree: first_to_agree || false }) } if (cve_ids) { addData({ cve_ids }) } const changes = getActionChanges(item) if (changes) { activity.changes = changes } if (bounty_amount || bonus_amount) { const bounty = Number(bounty_amount || 0) const bonus = Number(bonus_amount || 0) activity.award = { id, bounty_amount: bounty, bonus_amount: bonus, total_amount: bounty + bonus, // currency, awarded_to: collaborator ? collaborator.username : null, } awarded_to.push(activity.award) } return activity }), } if (awarded_to.length > 0) { const getSum = (list, key) => { return list.reduce((sum, item) => { if (item[key] >= 0) { sum += item[key] } return sum }, 0) } const countBounties = awards => { let bounties = awards.reduce((bounties, bounty) => { const hacker = bounty.awarded_to if (!bounties[hacker]) { bounties[hacker] = {} } for (const [param, value] of Object.entries(bounty)) { if (!(param && param.includes('_amount'))) { bounties[hacker][param] = value continue } if (!bounties[hacker][param]) { bounties[hacker][param] = 0 } bounties[hacker][param] += value } return bounties }, {}) bounties = Object.values(bounties) return { bounty_amount: getSum(bounties, 'bounty_amount'), bonus_amount: getSum(bounties, 'bonus_amount'), total_amount: getSum(bounties, 'total_amount'), // currency: default_currency, awarded_to: bounties, } } json.award = countBounties(awarded_to) } if (isIBB) { json.program.ibb = true } if (normalize) { json = norm(json) } return string ? JSON.stringify(json) : json } const getActionChanges = ({ additional_data, ...action }) => { const data = { ...additional_data, ...action } const fields = Object.keys(data).filter(field => /^(old|new|added|removed)_/.test(field), ) const changes = {} fields.forEach(field => { const [actn, key] = field.split('_') const actionType = actn.replace('added', 'new').replace('removed', 'old') changes[key] = { ...(changes[key] || null), [actionType]: data[field], } }) for (const param in changes) { if (param == 'weaknesses') { const { old: [old = null], new: [newValue = null], } = changes[param] changes.weakness = { old, new: newValue, } delete changes[param] } } return Object.keys(changes).length > 0 ? changes : null } export default format