scc-serverless
Version:
Deliver Cloudflare logs to Google Cloud Security Command Center
290 lines (252 loc) • 7.66 kB
JavaScript
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))
})
}