Skip to content

Commit

Permalink
feat(twistlock): add support for twistlock reports
Browse files Browse the repository at this point in the history
  • Loading branch information
error418 committed Jun 28, 2019
1 parent aabfc18 commit 804e021
Show file tree
Hide file tree
Showing 12 changed files with 1,051 additions and 1 deletion.
9 changes: 8 additions & 1 deletion src/core/template/template-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class TemplateEngine {
this.addFilter("gateConditionIcon", this.qualityGateConditionIconFilter);
this.addFilter("delta", this.deltaFilter);
this.addFilter("fixed", this.fixedFilter);
this.addFilter("keys", this.listKeys);
}

/** Gets and fills a template
Expand Down Expand Up @@ -56,12 +57,18 @@ export class TemplateEngine {
return str;
}

public listKeys(object: any) {
if (object) {
return Object.keys(object);
}
}
}

export enum Templates {
/** Template for GitHub Check Run summaries */
CHECK_RUN_SUMMARY = "check-run/summary.md.tpl",
ZAP_SCAN = "zap/report.md.tpl"
ZAP_SCAN = "zap/report.md.tpl",
TWISTLOCK_SCAN = "twistlock/scan.md.tpl"
}

export interface TemplateData {
Expand Down
6 changes: 6 additions & 0 deletions src/twistlock/config.ts
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"
}
25 changes: 25 additions & 0 deletions src/twistlock/events.ts
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;
}
}
39 changes: 39 additions & 0 deletions src/twistlock/model.ts
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;
}
}
73 changes: 73 additions & 0 deletions src/twistlock/status-emitter.ts
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;
36 changes: 36 additions & 0 deletions src/twistlock/twistlock.ts
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;
}
}
100 changes: 100 additions & 0 deletions src/twistlock/webhook.ts
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;
43 changes: 43 additions & 0 deletions templates/twistlock/scan.md.tpl
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 %}
Loading

0 comments on commit 804e021

Please sign in to comment.