diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..618f328 --- /dev/null +++ b/.babelrc @@ -0,0 +1,5 @@ +{ + "plugins": [ + "@babel/plugin-syntax-bigint" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 5360965..01ce690 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,12 @@ If the finding received as part of notification signifies MEDIUM/HIGH severity, **sendToSlack**: If the finding received as part of notification signifies LOW severity, then this particular function will be called to send a Slack alert. This function builds message with necessary information (example: findings id, source etc) and post it to given Slack channel. Before we begin sent Slack alert, we will need a slack channel and a webhook url for the same. For more information, see this [link](https://api.slack.com/incoming-webhooks#create_a_webhook) +**sendToEventstream**: +All the findings that are received as part of this notification webhook will be put into a configured event stream (kafka) topic. This function will act as kafka producer. + +**sendToLogDNA**: +All the findings that are received as part of this notification webhook will be send to a configured logDNA instance. + **main**: IBM Cloud Functions requires a function called main to exist as an entry point for the action. The params object contains the body of the incoming request. Security Advisor notification body contains a single JSON object with a single property called **data** that holds the signed JWT string as its value. When we obtained the public key, we can use it to verify the JWT signature. We’ll use the jsonwebtoken library’s verify function. This function receives the JWT string and a public key and returns the payload decoded if the signature is valid. If not, it will throw an error. @@ -45,17 +51,17 @@ When we obtained the public key, we can use it to verify the JWT signature. We - **slackChannel** : Slack channel name - **GITHUB_ACCESS_TOKEN** : Developer access token generated using GitHub - **GITHUB_API_URL** : GitHub API url for your repo + - **sendTologDNA** : True/False, If set to True will send the finding to configured logDNA instance. + - **logDNAEndpoint** : logDNA ingestion endpoint, example: https://logs.us-south.logging.cloud.ibm.com + - **logDNAIngestionKey** : logDNA ingestion key from logDNA instance UI. + - **sendToEventstream** : True/False, If set to True will send the finding to configured Eventstream instance. + - **kafkaMetadataBrokerList** : Kafka metadata broker list from Event stream instance service credentials. + - **kafkaSaslUsername** : Kafka user name from Event stream instance service credentials + - **kafkaSaslPassword** : kafka user password from Event stream instance service credentials + - **kafkaTopic** : Kafka topic name + 7. Bind parameters to your action. - `ibmcloud fn action update security-advisor-notifier --param-file params.json` - Verify using `ibmcloud fn action get security-advisor-notifier parameters` 8. Get the URL endpoint for your action. - ```echo `ibmcloud fn action get security-advisor-notifier --url | grep 'https'`'.json'``` - - - ## Create Cloud function action using UI - - 1. To create a Cloud Function, go to the Functions from left nav bar in [IBM Cloud Dashboard](https://cloud.ibm.com/), select the Actions tab, click the Create button, and then click Create a new action. Give the action a name, chose the default package, select a Node.js runtime (the sample code in this repo `src/notifier.js` is compatible with Node.js 8), and click the Create button. - 2. Copy the code from `src/notifier.js` to your Cloud Function. - 3. Add required parameters mentioned in `params.json` by clicking `Parameters` from the left nav of the Cloud Functions UI. - 4. Select `Endpoints` from the left nav of the Cloud Functions UI, check the Enable as Web Function checkbox, and click the Save button. Copy the URL that was added at the bottom of the Web Action section. - diff --git a/package.json b/package.json index 7c8b1c8..c47b3e5 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,18 @@ "deploy": "ibmcloud fn action update security-advisor-notifier dist/bundle.js --kind nodejs:10 --web true" }, "dependencies": { + "@babel/core": "^7.11.6", + "@babel/plugin-syntax-bigint": "^7.8.3", "axios": "^0.19.0", + "babel-loader": "^8.1.0", + "date-and-time": "^0.14.1", "jsonwebtoken": "^8.5.1", + "kafkajs": "1.14.0", "log4js": "^4.4.0", "request": "^2.88.0" }, "devDependencies": { - "webpack": "^3.8.1" + "webpack": "^4.44.2", + "webpack-cli": "^3.3.12" } } diff --git a/params.json b/params.json index 680ca41..c90573d 100644 --- a/params.json +++ b/params.json @@ -6,5 +6,13 @@ "notificationChannelUrl": "https://.secadvisor.cloud.ibm.com/notifications", "GITHUB_API_URL": "https://github.ibm.com/api/v3/repos///issues", "slackEndpoint": "", - "slackChannel": "" + "slackChannel": "", + "sendTologDNA": "", + "logDNAEndpoint": "", + "logDNAIngestionKey": "", + "sendToEventstream": "", + "kafkaMetadataBrokerList": "", + "kafkaSaslUsername": "", + "kafkaSaslPassword": "", + "kafkaTopic": "" } diff --git a/src/notifier.js b/src/notifier.js index efe14bc..490b303 100644 --- a/src/notifier.js +++ b/src/notifier.js @@ -1,9 +1,9 @@ const jwt = require("jsonwebtoken"); const axios = require("axios"); var log4js = require("log4js"); -var logger = log4js.getLogger(); +const date = require('date-and-time'); +var logger = log4js.getLogger('security-advisor-notification-webhook'); logger.level = "info"; -const request = require("request"); const webhookInternalErrorResponse = { err: "WebHook Internal Error." }; @@ -29,9 +29,8 @@ async function downloadPublicKey(accessToken, accountId, params) { } }; - const url = `${ - params.notificationChannelUrl - }/v1/${accountId}/notifications/public_key`; + const url = `${params.notificationChannelUrl + }/v1/${accountId}/notifications/public_key`; const response = await axios.get(url, config); logger.info(`Downloaded public key for account ${accountId}`); return response.data.publicKey; @@ -45,6 +44,91 @@ async function downloadPublicKey(accessToken, accountId, params) { } } +async function sendToLogDNA(finding, params) { + try { + const basicAuth = Buffer.from(`${params.logDNAIngestionKey}:`).toString('base64') + const config = { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Authorization: `Basic ${basicAuth}` + } + }; + const body = { + "lines": [ + { + "timestamp": date.format(new Date(), 'YYYY-MM-DDTHH:mm:ss.SSZ', true), + "line": JSON.stringify(finding), + "app": "cloud function", + "level": "INFO", + "meta": { + "labels": "IBM Security Advisor" + } + } + ] + } + const url = `${params.logDNAEndpoint}/logs/ingest?hostname=security-advisor-notification-webhook&tags=security-advisor&now=${Date.now()}` + + await axios.post(url, body, config); + } catch (err) { + logger.error( + `Error while sending finding ${finding["id"] + } to logDNA : ${err}` + ); + throw err; + } +} + +async function sendToEventstream(finding, params) { + const { Kafka } = require('kafkajs'); + const topic = params.kafkaTopic; + const brokers = params.kafkaMetadataBrokerList.split(',') + const kafka = new Kafka({ + clientId: 'security-advisor-notification-webhook', + kafka_topic: topic, + brokers: brokers, + sasl: { + mechanism: 'plain', + username: params.kafkaSaslUsername, + password: params.kafkaSaslPassword + }, + ssl: true, + connectionTimeout: 3000, + authenticationTimeout: 1000, + reauthenticationThreshold: 10000, + }); + // 2.Creating Kafka Producer + const producer = kafka.producer(); + const runProducer = async () => { + // 3.Connecting producer to kafka broker. + try { + await producer.connect() + await producer.send({ + topic: topic, + messages: + [{ value: JSON.stringify(finding) }], + }) + await producer.disconnect() + } catch (err) { + logger.error( + `Error while sending finding ${finding["id"] + } to Eventstream : ${err}` + ); + throw err; + } + } + try { + await runProducer() + logger.info(`Finding ${finding["id"]} is published to '${topic}'`); + } catch (err) { + logger.error( + `Error while sending finding ${finding["id"] + } to Eventstream : ${err}` + ); + throw err; + } +} + async function createGitHubIssue(finding, params) { try { var issueDesc = `**Source**: ${finding["payload"]["reported_by"]["title"]}\n`; @@ -52,9 +136,8 @@ async function createGitHubIssue(finding, params) { issueDesc = issueDesc + `**Severity**: ${finding["severity"]}\n`; issueDesc = issueDesc + `[View in Security Advisor Dashboard](${finding["issuer-url"]})\n`; var body = { - title: `${ - finding["severity"] - } severity finding reported by IBM Security Advisor`, + title: `${finding["severity"] + } severity finding reported by IBM Security Advisor`, body: issueDesc, labels: ["IBM Security Advisor"] }; @@ -70,8 +153,7 @@ async function createGitHubIssue(finding, params) { const response = await axios.post(params.GITHUB_API_URL, body, config); } catch (err) { logger.error( - `Error while creating GitHub issue for finding ${ - finding["id"] + `Error while creating GitHub issue for finding ${finding["id"] }: ${JSON.stringify(err)}` ); throw err; @@ -89,7 +171,7 @@ async function sendToSlack(finding, params) { attachments: [ { color: "#FFD300", - text: "```" + messageDesc + "```", + text: "```" + messageDesc + "```", mrkdwn_in: ["text"], actions: [ { @@ -172,7 +254,7 @@ async function main(params) { ); await sendToSlack(finding, params); } catch (err) { - logger.error(`Slack error : JSON.stringify(err)`); + logger.error(`Slack error : ${JSON.stringify(err)}`); return { err: "Couldn't notify slack" }; } } else if (severity === "high" || severity === "medium") { @@ -188,6 +270,36 @@ async function main(params) { return { err: "Couldn't create github issue" }; } } + + if (params.sendToLogDNA === "True") { + try { + logger.info( + `Received a finding ${finding["id"]}. Sending to logDNA.` + ); + await sendToLogDNA(finding, params); + logger.info( + `Successfully send finding ${finding["id"]} to logDNA.` + ); + } catch (err) { + logger.error(`logDNA error : ${err}`); + return { err: "Couldn't send to logDNA" }; + } + } + + if (params.sendToEventstream === "True") { + try { + logger.info( + `Received a finding ${finding["id"]}. Sending to Event stream.` + ); + await sendToEventstream(finding, params); + logger.info( + `Successfully send finding ${finding["id"]} to Event stream.` + ); + } catch (err) { + logger.error(`Eventstream error : ${err}`); + return { err: "Couldn't send to Eventstream" }; + } + } } exports.main = main; diff --git a/webpack.config.js b/webpack.config.js index 7f0d9a8..fa90d65 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,5 +5,13 @@ path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, - target: 'node' + target: 'node', + module: { + rules: [ + { + test: /\.js$/, + loader: 'babel-loader' + } + ] + } };