UNPKG

scc-serverless

Version:

Deliver Cloudflare logs to Google Cloud Security Command Center

290 lines (252 loc) 7.66 kB
'use strict' require('dotenv').config() const { BigQuery } = require('@google-cloud/bigquery') const { ErrorReporting } = require('@google-cloud/error-reporting') const fs = require('fs-extra') const { red, white, green, bold, blue } = require('kleur') const colos = require('./colos.json') const Datastore = require('@google-cloud/datastore') const datastore = new Datastore({ projectId: process.env.PROJECT_ID, keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS }) const SC = require('@google-cloud/security-center').v1beta1 const securityCenter = new SC.SecurityCenterClient({ projectId: process.env.PROJECT_ID, keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS }) const { telegram } = require('./telegram') const success = msg => console.log(`${green().bold(`[✔] `)}${white(msg)}`) const info = msg => console.log(`${blue(`[cse] `)}${white(msg)}`) const err = msg => console.log(`${red().bold(`[!] `)}${white(msg)}`) // let serviceKey = { // pseudoFile: new Vinyl({ // cwd: '/', // base: '/', // path: './CSE_key_latest.json', // contents: Buffer.from(jsesc(keyObj)) // }), // get contents () { // let json // try { // json = keyObj // console.log(json.KEY) // } catch (e) { // err(`buffer corrupted, rewriting key contents to disk.`) // json = process.env.GOOGLE_APPLICATION_CREDENTIALS // return json // } // return json.KEY // } // } class CSE { constructor ({ orgPath, source, log }) { this.orgPath = orgPath this.source = source this.log = log this._assets = [this.orgPath] this.finding = {} this.fields = require('./config/fields.json').fields this.fields = Array.from([this.fields.EdgePathingStatus, this.fields.EdgePathingSrc]) this.rationale = new Map(Object.entries(this.fields[0])) } static colos () { return colos } // set finding (obj) { // Object.assign(this._finding, obj) // } // get finding () { // return this._finding // } get assets () { return this._assets } set assets (assetArr) { Array.prototype.push.apply(this._assets, [assetArr]) } update () { let fieldMask = { mask: 'attribute.sourceProperties,attribute.resourceName,attribute.eventTime' } info('update() called') return securityCenter.updateFinding({ finding: this.finding, updateMask: fieldMask }).then(responses => { const outcome = responses[0] success(this.finding.name) return outcome }).catch(e => { err(e) }) } // Map EdgeColoID to the city where the colo resides static getColo (edgeColoID) { let id = Number.parseInt(edgeColoID, 10) let colos = CSE.colos() if (edgeColoID <= 172) return String(colos[id].colo_alias) const inChina = colos.slice(172).findIndex(colo => colo.colo_id === edgeColoID) if (inChina > -1) return String(colos[id].colo_alias) return 'San Francisco, CA' } listStream () { const formattedParent = this.source return securityCenter.listFindingsStream({ parent: formattedParent }) .on('data', element => { info(JSON.stringify(element, null, 2)) }).on('error', err => { console.log(err) }) } formatFinding () { let log = this.log this.finding = { externalUri: `https://dash.cloudflare.com/`, sourceProperties: { Action: { stringValue: log.EdgePathingStatus, kind: 'stringValue' }, Status: { stringValue: `${log.ClientRequestProtocol} ${log.EdgeResponseStatus}`, kind: 'stringValue' }, Host: { stringValue: log.ClientRequestHost, kind: 'stringValue' }, URI: { stringValue: `${log.ClientRequestMethod} ${log.ClientRequestURI}`, kind: 'stringValue' }, Country: { stringValue: log.ClientCountry.toUpperCase(), kind: 'stringValue' }, Location: { stringValue: CSE.getColo(log.EdgeColoID), king: 'stringValue' }, ClientIP: { stringValue: log.ClientIP, kind: 'stringValue' }, ClientASN: { stringValue: log.ClientASN, kind: 'stringValue' }, UA: { stringValue: log.ClientRequestUserAgent, kind: 'stringValue' }, Referer: { stringValue: log.ClientRequestReferer, kind: 'stringValue' } } } let category = log.EdgeResponseStatus === 429 ? 'Block: Rate Limit' : log.EdgeResponseStatus if (log.WAFRuleMessage.length > 2) { category = log.WAFRuleMessage this.finding.WAFAction = log.WAFAction this.finding.WAFProfile = log.WAFProfile this.finding.sourceProperties.Action = { stringValue: log.WAFAction, kind: 'stringValue' } } else { [log.EdgePathingStatus, log.EdgePathingSrc].forEach(ratch => { if (this.rationale.has(ratch)) category = this.rationale.get(ratch) }) } this.assets = [log.originIP] Object.assign(this.finding, { name: `${this.source}/findings/${log.RayID}`, state: 'ACTIVE', category: category, resourceName: `${this.assets[0]}` }) let eventTime = { seconds: `${(Date.now().toString()).slice(0, 10)}`, nanos: Number.parseInt(`${Date.now().toString().slice(0, 9)}`) } this.finding.eventTime = eventTime console.log(this.finding) return this } static async config () { let orgPath = `organizations/${process.env.GCLOUD_ORG}` let sources = await securityCenter.listSources({ parent: orgPath }) sources = sources[0] let getSource = sources.filter(src => src.displayName === 'Cloudflare') if (getSource[0] !== 'Cloudflare') { getSource = await securityCenter.createSource({ parent: orgPath, source: { displayName: 'Cloudflare' } }) sources = sources[0] } else { sources = getSource[0] } info(`Using source: ${sources.displayName}, in org ${orgPath}`) return { orgPath: orgPath, source: sources.name } } static async scc () { let config = await CSE.config() let bigquery = new BigQuery() const queries = ['./queries/threats.txt', './queries/rate_limit.txt'] const runQueries = queries.map(async qry => { qry = fs.readFileSync(qry) qry = `${qry}`.replace('BQ_DATASET', process.env.BQ_DATASET) info(`Running query: ${qry}`) bigquery.createQueryStream(qry) .on('error', console.error) .on('data', async function (row) { let finding, cse try { config.log = row cse = new CSE(config) finding = await cse.formatFinding().update() } catch (e) { err(e) } return finding // row is a result from your query. }) .on('end', function () { success('Findings delivered. Waiting on response ...') // All rows retrieved. }) }) // log them in sequence for (const runQuery of runQueries) { console.log(await runQuery) } } } exports.CSE = CSE exports.matchAssets = (row, { onDone }) => { const db = { namespace: 'cfscc', tbl: 'events' } let _key = datastore.key({ namespace: db.namespace, path: [db.tbl, `${row['RayID']}`] }) let entry = { key: _key, data: row } datastore.save(entry).then(data => { return telegram.emit('findings_formatted', JSON.stringify(row)) }) }