Skip to content
This repository was archived by the owner on May 18, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"plugins": [
"@babel/plugin-syntax-bigint"
]
}
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment on lines +9 to +12
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like devDependencies.

"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"
}
}
10 changes: 9 additions & 1 deletion params.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,13 @@
"notificationChannelUrl": "https://<region>.secadvisor.cloud.ibm.com/notifications",
"GITHUB_API_URL": "https://github.ibm.com/api/v3/repos/<Repo Owner>/<Repo Name>/issues",
"slackEndpoint": "",
"slackChannel": ""
"slackChannel": "",
"sendTologDNA": "",
"logDNAEndpoint": "",
"logDNAIngestionKey": "",
"sendToEventstream": "",
"kafkaMetadataBrokerList": "",
"kafkaSaslUsername": "",
"kafkaSaslPassword": "",
"kafkaTopic": ""
}
136 changes: 124 additions & 12 deletions src/notifier.js
Original file line number Diff line number Diff line change
@@ -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." };

Expand All @@ -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;
Expand All @@ -45,16 +44,100 @@ 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),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we can just use new Date().toISOString()

"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`;
issueDesc = issueDesc + `**Finding**: ${finding["id"]}\n`;
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"]
};
Expand All @@ -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;
Expand All @@ -89,7 +171,7 @@ async function sendToSlack(finding, params) {
attachments: [
{
color: "#FFD300",
text: "```" + messageDesc + "```",
text: "```" + messageDesc + "```",
mrkdwn_in: ["text"],
actions: [
{
Expand Down Expand Up @@ -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") {
Expand All @@ -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;
10 changes: 9 additions & 1 deletion webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,13 @@
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
target: 'node'
target: 'node',
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader'
}
]
}
};