Skip to content

Commit 9a48ae6

Browse files
authored
Merge pull request #1004 from snyk/feat/sysdig-integration
RUN-2052 Feat/sysdig integration
2 parents f02a6b7 + d692019 commit 9a48ae6

File tree

15 files changed

+566
-26
lines changed

15 files changed

+566
-26
lines changed

snyk-monitor/README.md

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ abcd1234-abcd-1234-abcd-1234abcd1234
2525
```
2626
The Snyk Integration ID is used in the `--from-literal=integrationId=` parameter in the next step.
2727

28-
2. If you are not using any private registries, create a Kubernetes secret called `snyk-monitor` containing the Snyk Integration ID from the previous step running the following command:
28+
2. (Optional) If you are not using any private registries, create a Kubernetes secret called `snyk-monitor` containing the Snyk Integration ID from the previous step running the following command:
2929
```shell
3030
kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=abcd1234-abcd-1234-abcd-1234abcd1234
3131
```
3232
Continue to Helm installation instructions below.
3333

34-
3. If you're using a private registry, you should create a `dockercfg.json` file. The `dockercfg` file is necessary to allow the monitor to look up images in private registries. Usually your credentials can be found in `$HOME/.docker/config.json`. These must also be added to the `dockercfg.json` file.
34+
3. (Optional) If you're using a private registry, you should create a `dockercfg.json` file. The `dockercfg` file is necessary to allow the monitor to look up images in private registries. Usually your credentials can be found in `$HOME/.docker/config.json`. These must also be added to the `dockercfg.json` file.
3535

3636
Create a file named `dockercfg.json`. Store your credentials in there; it should look like this:
3737

@@ -77,12 +77,12 @@ Finally, create the secret in Kubernetes by running the following command:
7777
kubectl create secret generic snyk-monitor -n snyk-monitor --from-file=./dockercfg.json --from-literal=integrationId=abcd1234-abcd-1234-abcd-1234abcd1234
7878
```
7979

80-
4. If your private registry requires installing certificates (*.crt, *.cert, *.key only) please put them in a folder and create the following ConfigMap:
80+
4. (Optional) If your private registry requires installing certificates (*.crt, *.cert, *.key only) please put them in a folder and create the following ConfigMap:
8181
```shell
8282
kubectl create configmap snyk-monitor-certs -n snyk-monitor --from-file=<path_to_certs_folder>
8383
```
8484

85-
5. If you are using an insecure registry or your registry is using unqualified images, you can provide a `registries.conf` file. See [the documentation](https://github.com/containers/image/blob/master/docs/containers-registries.conf.5.md) for information on the format and examples.
85+
5. (Optional) If you are using an insecure registry or your registry is using unqualified images, you can provide a `registries.conf` file. See [the documentation](https://github.com/containers/image/blob/master/docs/containers-registries.conf.5.md) for information on the format and examples.
8686

8787
Create a file named `registries.conf`, see example adding an insecure registry:
8888

@@ -128,6 +128,31 @@ If '--reset-values' is specified, this is ignored.
128128

129129
If running with Operator Lifecycle Manager (OLM) then OLM will handle upgrades for you when you request to install the latest version. This applies to OpenShift (OCP) and regular installations of OLM.
130130

131+
## Sysdig Integration ##
132+
133+
We have partnered with Sysdig to enrich the issues detected by Snyk for workloads with runtime data provided by Sysdig.
134+
135+
In order for the integration with Sysdig to work, the Snyk monitor requires an extra Secret in the `snyk-monitor` namespace. The Secret name is `sysdig-eve-secret`.
136+
137+
Please refer to the [Sysdig Secret installation guide](https://docs.sysdig.com/en/docs/sysdig-secure/integrate-effective-vulnerability-exposure-with-snyk/#copy-the-sysdig-secret) to install the Secret. Once the Sysdig Secret is installed, you need to copy it over to the snyk-monitor namespace:
138+
139+
```bash
140+
kubectl get secret sysdig-eve-secret -n sysdig-agent -o yaml | grep -v '^\s*namespace:\s' | kubectl apply -n snyk-monitor -f -
141+
```
142+
143+
To enable Snyk to integrate with Sysdig and collect information about packages executed at runtime, use `--set sysdig.enabled=true` when installing the snyk-monitor:
144+
145+
```bash
146+
helm upgrade --install snyk-monitor snyk-charts/snyk-monitor \
147+
--namespace snyk-monitor \
148+
--set clusterName="Production cluster" \
149+
--set sysdig.enabled=true
150+
```
151+
152+
> NOTE: The above command should be executed right after installing Sysdig. This will upgrade or install the snyk monitor, to allow the detection of Sysdig in the cluster.
153+
154+
The snyk-monitor will now collect data from Sysdig every 4 hours.
155+
131156
## Setting up proxying ##
132157

133158
Proxying traffic through a forwarding proxy can be achieved by setting the following values in the Helm chart:

snyk-monitor/templates/deployment.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,20 @@ spec:
132132
value: {{ quote .Values.skopeo.compression.level }}
133133
- name: SNYK_WORKERS_COUNT
134134
value: {{ quote .Values.workers.count }}
135+
{{- if .Values.sysdig.enabled }}
136+
- name: SNYK_SYSDIG_ENDPOINT
137+
valueFrom:
138+
secretKeyRef:
139+
name: {{ .Values.sysdig.secretName }}
140+
key: endpoint
141+
optional: true
142+
- name: SNYK_SYSDIG_TOKEN
143+
valueFrom:
144+
secretKeyRef:
145+
name: {{ .Values.sysdig.secretName }}
146+
key: token
147+
optional: true
148+
{{- end }}
135149
{{- with .Values.envs }}
136150
{{- toYaml . | trim | nindent 10 -}}
137151
{{- end }}

snyk-monitor/values.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,7 @@ skopeo:
143143

144144
workers:
145145
count: 10
146+
sysdig:
147+
enabled: false
148+
namespace: sysdig-agent
149+
secretName: sysdig-eve-secret

src/common/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ config.EXCLUDED_NAMESPACES = loadExcludedNamespaces();
4545
config.WORKERS_COUNT = Number(config.WORKERS_COUNT) || 10;
4646
config.SKOPEO_COMPRESSION_LEVEL = Number(config.SKOPEO_COMPRESSION_LEVEL) || 6;
4747

48+
// return Sysdig endpoint information
49+
if (config.SYSDIG_ENDPOINT && config.SYSDIG_TOKEN) {
50+
config.SYSDIG_ENDPOINT = config.SYSDIG_ENDPOINT.trim();
51+
config.SYSDIG_TOKEN = config.SYSDIG_TOKEN.trim();
52+
}
53+
4854
/**
4955
* Important: we delete the following env vars because we don't want to proxy requests to the Kubernetes API server.
5056
* The Kubernetes client library would honor the NO/HTTP/HTTPS_PROXY env vars.

src/data-scraper/index.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { logger } from '../common/logger';
2+
import { config } from '../common/config';
3+
import { sendRuntimeData } from '../transmitter';
4+
import { constructRuntimeData } from '../transmitter/payload';
5+
import { retryRequest } from '../transmitter';
6+
import { IRuntimeImagesResponse } from '../transmitter/types';
7+
import { NeedleOptions } from 'needle';
8+
import { Agent as HttpsAgent } from 'https';
9+
10+
function isSuccessStatusCode(statusCode: number | undefined): boolean {
11+
return statusCode !== undefined && statusCode >= 200 && statusCode < 300;
12+
}
13+
14+
export async function scrapeData(): Promise<void> {
15+
const base: string = config.SYSDIG_ENDPOINT;
16+
const header: string = `Bearer ${config.SYSDIG_TOKEN}`;
17+
18+
const url: string = base + '/v1/runtimeimages';
19+
// limit: min 1, max 500, default 250
20+
const limit: number = 10;
21+
const reqOptions: NeedleOptions = {
22+
agent: new HttpsAgent({
23+
keepAlive: true,
24+
// We agreed with Sysdig to skip TLS certificates validation for HTTPS connection.
25+
rejectUnauthorized: false,
26+
}),
27+
headers: {
28+
authorization: header,
29+
},
30+
};
31+
32+
let cursor: string = '';
33+
while (true) {
34+
try {
35+
logger.info({ cursor }, 'attempting to get runtime images');
36+
37+
const { response, attempt } = await retryRequest(
38+
'get',
39+
`${url}?limit=${limit}&cursor=${cursor}`,
40+
{},
41+
reqOptions,
42+
);
43+
if (!isSuccessStatusCode(response.statusCode)) {
44+
throw new Error(`${response.statusCode} ${response.statusMessage}`);
45+
}
46+
47+
logger.info(
48+
{
49+
attempt,
50+
cursor,
51+
},
52+
'runtime images received successfully',
53+
);
54+
55+
const responseBody: IRuntimeImagesResponse = response.body;
56+
const runtimeDataPayload = constructRuntimeData(responseBody.data);
57+
logger.info({}, 'sending runtime data upstream');
58+
await sendRuntimeData(runtimeDataPayload);
59+
60+
cursor = responseBody.page.next || '';
61+
if (!cursor) {
62+
break;
63+
}
64+
} catch (error) {
65+
logger.error(
66+
{
67+
error,
68+
cursor,
69+
},
70+
'could not get runtime images',
71+
);
72+
break;
73+
}
74+
}
75+
}

src/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { beginWatchingWorkloads } from './supervisor/watchers';
1010
import { loadAndSendWorkloadEventsPolicy } from './common/policy';
1111
import { sendClusterMetadata } from './transmitter';
1212
import { setSnykMonitorAgentId } from './supervisor/agent';
13+
import { scrapeData } from './data-scraper';
1314

1415
process.on('uncaughtException', (err) => {
1516
if (state.shutdownInProgress) {
@@ -68,4 +69,20 @@ setImmediate(async function setUpAndMonitor(): Promise<void> {
6869
await sendClusterMetadata();
6970
await loadAndSendWorkloadEventsPolicy();
7071
await monitor();
72+
73+
const interval: number = 4 * 60 * 60 * 1000; // 4 hours in milliseconds
74+
if (config.SYSDIG_ENDPOINT && config.SYSDIG_TOKEN) {
75+
setInterval(async () => {
76+
try {
77+
await scrapeData();
78+
} catch (error) {
79+
logger.error(
80+
{ error },
81+
'an error occurred while scraping runtime data',
82+
);
83+
}
84+
}, interval).unref();
85+
} else {
86+
logger.info({}, 'Sysdig integration not detected');
87+
}
7188
});

src/transmitter/index.ts

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { parse } from 'url';
21
import { queue } from 'async';
32
import needle from 'needle';
43
import sleep from 'sleep-promise';
@@ -17,6 +16,7 @@ import {
1716
IDependencyGraphPayload,
1817
IWorkloadEventsPolicyPayload,
1918
IClusterMetadataPayload,
19+
IRuntimeDataPayload,
2020
} from './types';
2121
import { getProxyAgent } from './proxy';
2222

@@ -27,24 +27,29 @@ interface KubernetesUpstreamRequest {
2727
| IDependencyGraphPayload
2828
| ScanResultsPayload
2929
| IWorkloadMetadataPayload
30-
| IDeleteWorkloadPayload;
30+
| IDeleteWorkloadPayload
31+
| IClusterMetadataPayload
32+
| IRuntimeDataPayload;
3133
}
3234

3335
const upstreamUrl =
3436
config.INTEGRATION_API || config.DEFAULT_KUBERNETES_UPSTREAM_URL;
3537

36-
let agent = new HttpAgent({
38+
let httpAgent = new HttpAgent({
3739
keepAlive: true,
3840
});
3941

40-
if (parse(upstreamUrl).protocol?.startsWith('https')) {
41-
agent = new HttpsAgent({
42-
keepAlive: true,
43-
});
42+
let httpsAgent = new HttpsAgent({
43+
keepAlive: true,
44+
});
45+
46+
function getAgent(u: string): HttpAgent {
47+
const url = new URL(u);
48+
return url.protocol === 'https:' ? httpsAgent : httpAgent;
4449
}
4550

4651
// Async queue wraps around the call to retryRequest in order to limit
47-
// the number of requests in flight to Homebase at any one time.
52+
// the number of requests in flight to kubernetes upstream at any one time.
4853
const reqQueue = queue(async function (req: KubernetesUpstreamRequest) {
4954
return await retryRequest(req.method, req.url, req.payload);
5055
}, config.REQUEST_QUEUE_LENGTH);
@@ -60,7 +65,7 @@ export async function sendDepGraph(
6065
const request: KubernetesUpstreamRequest = {
6166
method: 'post',
6267
url: `${upstreamUrl}/api/v1/dependency-graph`,
63-
payload: payload,
68+
payload,
6469
};
6570

6671
const { response, attempt } = await reqQueue.pushAsync(request);
@@ -91,7 +96,7 @@ export async function sendScanResults(
9196
const request: KubernetesUpstreamRequest = {
9297
method: 'post',
9398
url: `${upstreamUrl}/api/v1/scan-results`,
94-
payload: payload,
99+
payload,
95100
};
96101

97102
const { response, attempt } = await reqQueue.pushAsync(request);
@@ -127,7 +132,7 @@ export async function sendWorkloadMetadata(
127132
const request: KubernetesUpstreamRequest = {
128133
method: 'post',
129134
url: `${upstreamUrl}/api/v1/workload`,
130-
payload: payload,
135+
payload,
131136
};
132137

133138
const { response, attempt } = await reqQueue.pushAsync(request);
@@ -198,7 +203,7 @@ export async function deleteWorkload(
198203
const request: KubernetesUpstreamRequest = {
199204
method: 'delete',
200205
url: `${upstreamUrl}/api/v1/workload`,
201-
payload: payload,
206+
payload,
202207
};
203208

204209
const { response, attempt } = await reqQueue.pushAsync(request);
@@ -229,10 +234,11 @@ function isSuccessStatusCode(statusCode: number | undefined): boolean {
229234
return statusCode !== undefined && statusCode > 100 && statusCode < 400;
230235
}
231236

232-
async function retryRequest(
237+
export async function retryRequest(
233238
verb: NeedleHttpVerbs,
234239
url: string,
235240
payload: object,
241+
reqOptions: NeedleOptions = {},
236242
): Promise<IResponseWithAttempts> {
237243
const retry = {
238244
attempts: 3,
@@ -241,7 +247,8 @@ async function retryRequest(
241247
const options: NeedleOptions = {
242248
json: true,
243249
compressed: true,
244-
agent,
250+
agent: getAgent(url),
251+
...reqOptions,
245252
};
246253

247254
if (config.HTTP_PROXY || config.HTTPS_PROXY) {
@@ -317,11 +324,13 @@ export async function sendClusterMetadata(): Promise<void> {
317324
'attempting to send cluster metadata',
318325
);
319326

320-
const { response, attempt } = await retryRequest(
321-
'post',
322-
`${upstreamUrl}/api/v1/cluster`,
327+
const request: KubernetesUpstreamRequest = {
328+
method: 'post',
329+
url: `${upstreamUrl}/api/v1/cluster`,
323330
payload,
324-
);
331+
};
332+
333+
const { response, attempt } = await reqQueue.pushAsync(request);
325334
if (!isSuccessStatusCode(response.statusCode)) {
326335
throw new Error(`${response.statusCode} ${response.statusMessage}`);
327336
}
@@ -347,3 +356,46 @@ export async function sendClusterMetadata(): Promise<void> {
347356
);
348357
}
349358
}
359+
360+
export async function sendRuntimeData(
361+
payload: IRuntimeDataPayload,
362+
): Promise<void> {
363+
const logContext = {
364+
userLocator: payload.target.userLocator,
365+
cluster: payload.target.cluster,
366+
agentId: payload.target.agentId,
367+
identity: payload.identity,
368+
};
369+
370+
try {
371+
logger.info(logContext, 'attempting to send runtime data');
372+
373+
const request: KubernetesUpstreamRequest = {
374+
method: 'post',
375+
url: `${upstreamUrl}/api/v1/runtime-results`,
376+
payload,
377+
};
378+
379+
const { response, attempt } = await reqQueue.pushAsync(request);
380+
381+
if (!isSuccessStatusCode(response.statusCode)) {
382+
throw new Error(`${response.statusCode} ${response.statusMessage}`);
383+
}
384+
385+
logger.info(
386+
{
387+
attempt,
388+
...logContext,
389+
},
390+
'runtime data sent upstream successfully',
391+
);
392+
} catch (error) {
393+
logger.error(
394+
{
395+
error,
396+
...logContext,
397+
},
398+
'could not send runtime data',
399+
);
400+
}
401+
}

0 commit comments

Comments
 (0)