-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(twistlock): add support for twistlock reports
- Loading branch information
Showing
12 changed files
with
1,051 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export enum TwistlockConfig { | ||
ENABLED = "twistlock:enabled", | ||
SECRET = "twistlock:secret", | ||
CONTEXT = "twistlock:context", | ||
LOG_WEBHOOK_EVENTS = "twistlock:debug" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { SwingletreeEvent } from "../core/event/event-model"; | ||
import { TwistlockModel } from "./model"; | ||
|
||
export enum TwistlockEvents { | ||
TwistlockReportReceived = "twistlock:report-received" | ||
} | ||
|
||
abstract class TwistlockEvent extends SwingletreeEvent { | ||
constructor(eventType: TwistlockEvents) { | ||
super(eventType); | ||
} | ||
} | ||
|
||
export class TwistlockReportReceivedEvent extends TwistlockEvent { | ||
commitId: string; | ||
owner: string; | ||
repository: string; | ||
report: TwistlockModel.Report; | ||
|
||
constructor(report: TwistlockModel.Report) { | ||
super(TwistlockEvents.TwistlockReportReceived); | ||
|
||
this.report = report; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
export namespace TwistlockModel { | ||
|
||
export interface Report { | ||
results: Result[]; | ||
} | ||
|
||
interface Result { | ||
id: string; | ||
distro: string; | ||
complianceDistribution: SeverityCount; | ||
vulnerabilities?: Vulnerability[]; | ||
vulnerabilityDistribution: SeverityCount; | ||
} | ||
|
||
interface SeverityCount { | ||
critical: number; | ||
high: number; | ||
medium: number; | ||
low: number; | ||
total: number; | ||
} | ||
|
||
interface Vulnerability { | ||
id: string; | ||
status: string; | ||
cvss: number; | ||
vector: string; | ||
description: string; | ||
severity: string; | ||
packageName: string; | ||
packageVersion: string; | ||
link: string; | ||
riskFactors: any; | ||
} | ||
|
||
export interface Template { | ||
report: Report; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { injectable, inject } from "inversify"; | ||
import EventBus from "../core/event/event-bus"; | ||
import { GithubCheckRunWriteEvent } from "../core/event/event-model"; | ||
import { ChecksCreateParams } from "@octokit/rest"; | ||
import { ConfigurationService } from "../configuration"; | ||
import { LOGGER } from "../logger"; | ||
import { Templates } from "../core/template/template-engine"; | ||
import { TemplateEngine } from "../core/template/template-engine"; | ||
import { TwistlockEvents, TwistlockReportReceivedEvent } from "./events"; | ||
import { TwistlockConfig } from "./config"; | ||
import { TwistlockModel } from "./model"; | ||
|
||
@injectable() | ||
class TwistlockStatusEmitter { | ||
private readonly eventBus: EventBus; | ||
private readonly templateEngine: TemplateEngine; | ||
private readonly context: string; | ||
|
||
constructor( | ||
@inject(EventBus) eventBus: EventBus, | ||
@inject(ConfigurationService) configurationService: ConfigurationService, | ||
@inject(TemplateEngine) templateEngine: TemplateEngine | ||
) { | ||
this.eventBus = eventBus; | ||
this.templateEngine = templateEngine; | ||
this.context = configurationService.get(TwistlockConfig.CONTEXT); | ||
|
||
eventBus.register(TwistlockEvents.TwistlockReportReceived, this.reportReceivedHandler, this); | ||
} | ||
|
||
private getConclusion(event: TwistlockReportReceivedEvent): "action_required" | "success" { | ||
let conclusion: "success" | "action_required" = "success"; | ||
if (event.report.results && event.report.results.length > 0) { | ||
event.report.results.forEach((result) => { | ||
if (result.complianceDistribution.total + result.vulnerabilityDistribution.total > 0) { | ||
conclusion = "action_required"; | ||
} | ||
}); | ||
} | ||
|
||
return conclusion; | ||
} | ||
|
||
public async reportReceivedHandler(event: TwistlockReportReceivedEvent) { | ||
|
||
const checkRun: ChecksCreateParams = { | ||
name: this.context, | ||
owner: event.owner, | ||
repo: event.repository, | ||
status: "completed", | ||
conclusion: this.getConclusion(event), | ||
started_at: new Date().toISOString(), | ||
completed_at: new Date().toISOString(), | ||
head_sha: event.commitId | ||
}; | ||
|
||
const templateData: TwistlockModel.Template = { | ||
report: event.report | ||
}; | ||
|
||
checkRun.output = { | ||
title: `Twistlock scan result`, | ||
summary: this.templateEngine.template<TwistlockModel.Template>( | ||
Templates.ZAP_SCAN, | ||
templateData | ||
) | ||
}; | ||
|
||
this.eventBus.emit(new GithubCheckRunWriteEvent(checkRun)); | ||
} | ||
} | ||
|
||
export default TwistlockStatusEmitter; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import container from "../ioc-config"; | ||
import { SwingletreeComponent } from "../component"; | ||
import { WebServer } from "../core/webserver"; | ||
import { ConfigurationService } from "../configuration"; | ||
import { TwistlockConfig } from "./config"; | ||
import TwistlockWebhook from "./webhook"; | ||
import TwistlockStatusEmitter from "./status-emitter"; | ||
|
||
export class TwistlockPlugin extends SwingletreeComponent.Component { | ||
private enabled: boolean; | ||
|
||
constructor() { | ||
super("twistlock"); | ||
|
||
const configService = container.get<ConfigurationService>(ConfigurationService); | ||
this.enabled = configService.getBoolean(TwistlockConfig.ENABLED); | ||
} | ||
|
||
public run(): void { | ||
const webserver = container.get<WebServer>(WebServer); | ||
|
||
// register services to dependency injection | ||
container.bind<TwistlockWebhook>(TwistlockWebhook).toSelf().inSingletonScope(); | ||
container.bind<TwistlockStatusEmitter>(TwistlockStatusEmitter).toSelf().inSingletonScope(); | ||
|
||
// initialize Emitters | ||
container.get<TwistlockStatusEmitter>(TwistlockStatusEmitter); | ||
|
||
// add webhook endpoint | ||
webserver.addRouter("/webhook/twistlock", container.get<TwistlockWebhook>(TwistlockWebhook).getRoute()); | ||
} | ||
|
||
public isEnabled(): boolean { | ||
return this.enabled; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
"use strict"; | ||
|
||
import { Router, Request, Response, NextFunction } from "express"; | ||
import { injectable } from "inversify"; | ||
import { inject } from "inversify"; | ||
import EventBus from "../core/event/event-bus"; | ||
import { ConfigurationService } from "../configuration"; | ||
import * as BasicAuth from "basic-auth"; | ||
import { LOGGER } from "../logger"; | ||
import InstallationStorage from "../core/github/client/installation-storage"; | ||
import { TwistlockModel } from "./model"; | ||
import { TwistlockConfig } from "./config"; | ||
import { TwistlockReportReceivedEvent } from "./events"; | ||
|
||
/** Provides a Webhook for Sonar | ||
*/ | ||
@injectable() | ||
class TwistlockWebhook { | ||
private eventBus: EventBus; | ||
private configurationService: ConfigurationService; | ||
private installationStorage: InstallationStorage; | ||
|
||
constructor( | ||
@inject(EventBus) eventBus: EventBus, | ||
@inject(ConfigurationService) configurationService: ConfigurationService, | ||
@inject(InstallationStorage) installationStorage: InstallationStorage | ||
) { | ||
this.eventBus = eventBus; | ||
this.configurationService = configurationService; | ||
this.installationStorage = installationStorage; | ||
} | ||
|
||
private isWebhookEventRelevant(event: TwistlockModel.Report) { | ||
return event.results.length > 0; | ||
} | ||
|
||
private authenticationMiddleware(secret: string) { | ||
return (req: Request, res: Response, next: NextFunction) => { | ||
const auth = BasicAuth(req); | ||
if (auth && secret === auth.pass) { | ||
next(); | ||
} else { | ||
res.sendStatus(401); | ||
} | ||
}; | ||
} | ||
|
||
public getRoute(): Router { | ||
const router = Router(); | ||
const secret = this.configurationService.get(TwistlockConfig.SECRET); | ||
|
||
if (secret && secret.trim().length > 0) { | ||
router.use(this.authenticationMiddleware(secret)); | ||
} else { | ||
LOGGER.warn("Twistlock webhook is not protected. Consider setting a Twistlock secret in the Swingletree configuration."); | ||
} | ||
router.post("/", this.webhook); | ||
|
||
return router; | ||
} | ||
|
||
public webhook = async (req: Request, res: Response) => { | ||
LOGGER.debug("received Twistlock webhook event"); | ||
|
||
const org = req.query["org"]; | ||
const repo = req.query["repo"]; | ||
const sha = req.query["sha"]; | ||
|
||
if (this.configurationService.getBoolean(TwistlockConfig.LOG_WEBHOOK_EVENTS)) { | ||
LOGGER.debug(JSON.stringify(req.body)); | ||
} | ||
|
||
const webhookData: TwistlockModel.Report = req.body; | ||
|
||
if (org == null || repo == null || sha == null) { | ||
res.status(400).send("missing at least one of following query parameters: org, repo, sha"); | ||
return; | ||
} | ||
|
||
if (this.isWebhookEventRelevant(webhookData)) { | ||
const reportReceivedEvent = new TwistlockReportReceivedEvent(webhookData); | ||
reportReceivedEvent.commitId = sha; | ||
reportReceivedEvent.owner = org; | ||
reportReceivedEvent.repository = repo; | ||
|
||
// check if installation is available | ||
if (await this.installationStorage.getInstallationId(org)) { | ||
this.eventBus.emit(reportReceivedEvent); | ||
} else { | ||
LOGGER.info("ignored twistlock report for %s/%s. Swingletree may not be installed in this organization.", org, repo); | ||
} | ||
} else { | ||
LOGGER.debug("twistlock webhook data did not contain a report. This event will be ignored."); | ||
} | ||
|
||
res.sendStatus(204); | ||
} | ||
} | ||
|
||
export default TwistlockWebhook; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
{# Context Type: TwistlockModel.Template -#} | ||
|
||
{% if report.results.length > 0 %} | ||
{% for result in report.results -%} | ||
|
||
## Summary | ||
|
||
| Type | Critical | High | Medium | Low | Total | | ||
|:--- |:---:|:---:|:---:|:---:|:---:| | ||
| Compliance | {{ result.complianceDistribution.critical }} | {{ result.complianceDistribution.high }} | {{ result.complianceDistribution.medium }} | {{ result.complianceDistribution.low }} | {{ result.complianceDistribution.total }} | | ||
| Vulnerabilities | {{ result.vulnerabilityDistribution.critical }} | {{ result.vulnerabilityDistribution.high }} | {{ result.vulnerabilityDistribution.medium }} | {{ result.vulnerabilityDistribution.low }} | {{ result.vulnerabilityDistribution.total }} | | ||
|
||
|
||
## Vulnerabilities | ||
{% for vul in result.vulnerabilities | sort(true, false, "cvss") -%} | ||
### {{ vul.id }} | ||
|
||
| CVSS | Severity | Package | Version | | | ||
| --- | --- | --- | --- | --- | | ||
| {{ vul.cvss }} | {{ vul.severity }} | {{ vul.packageName }} | {{ vul.packageVersion }} | [details]({{ vul.link }}) | | ||
|
||
<details><summary>show description</summary><p><ul> | ||
<blockquote>{{ vul.description }}</blockquote> | ||
</ul></p></details> | ||
|
||
{%- if vul.vector %} | ||
|
||
#### Vector | ||
|
||
`{{ vul.vector }}` | ||
{% endif -%} | ||
|
||
{% if vul.vector -%} | ||
#### Risk Factors | ||
{% for factor in vul.riskFactors | keys -%} | ||
* {{ factor }} | ||
{% endfor %} | ||
{%- endif %} | ||
|
||
--- | ||
{% endfor %} | ||
{% endfor %} | ||
{% endif %} |
Oops, something went wrong.