Skip to content

Commit

Permalink
Add Snyk policy example
Browse files Browse the repository at this point in the history
  • Loading branch information
jkodroff committed Mar 22, 2024
1 parent c2051e7 commit 1c914c5
Show file tree
Hide file tree
Showing 15 changed files with 280 additions and 0 deletions.
71 changes: 71 additions & 0 deletions snyk-container-scan-policy-ts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# snyn-container-scan-policy

Scan Pulumi-managed Docker containers with Snyk and Pulumi Policy as Code:

- The code in the `infra` directory creates two `docker.Image` resources:
1. An image sourced from the `alpine` image, which does not have critical vulnerabilities.
1. An image sourced from the `debian` image, which has critical vulnerabilities.
- The code in the `policy` directory contains a Pulumi Policy Pack which calls the Snyk CLI. If the Snyk CLI fails (e.g. because it detects vulnerabilities), the resource will be considered in violation and the `pulumi preview` (or `pulumi up`) operation will fail.

To run the demo:

```bash
cd infra
pulumi preview --policy-pack ../policy
```

## Configuration Options

The `snyk-container-scan` policy has the following configurable options. These can be set by altering values in `policy-config.json` and calling Pulumi CLI with the `--policy-config` option, e.g.:

```bash
cd infra
pulumi preview --policy-pack ../policy --policy-pack-config policy-config.json
```

- `dockerfileScanning` (boolean): If set to `true`, Snyk will scan the Dockerfile of each image in the stack to scan for vulnerabilities in the upstream image. If set to `true`, `pulumiProgramAbsPath` must also be set to the absolute path on disk of the Pulumi program that contains the images so that the Snyk CLI can locate the Dockerfile.
- `excludeBaseImageVulns` (boolean): If true, do not show vulnerabilities introduced only by the base image. Defaults to `false`.
- `failOn`: Valid values: `all`, `upgradable`. Defaults to `all`.
- `pulumiProgramAbsPath` (string, optional): The absolute path on disk to the Pulumi (IaC) program. Used by Snyk to scan Dockerfiles for vulnerabilities. Only used (and required) if `dockerfileScanning` is set to `true`.
- `severityThreshold`: The minimum severity of found issues to report. If any issues are found at or above the minimum severity, the stack will contain violations. Valid values: `low`, `medium`, `high`, `critical`. Defaults to `critical`.

For additional information on Snyk CLI options, see: <https://docs.snyk.io/snyk-cli/commands/container-test>

## Enabling Dockerfile Scanning

Snyk can scan Dockerfiles for vulnerabilities. Because there's no direct relationship between the location on disk of a Pulumi program and any policy packs that might be running, we need to configure the Snyk policy to know where the Pulumi program is running.

The following script will add the needed absolute path configuration:

```bash
cd infra
./add-dockerfile-scanning.sh
```

Because Dockerfile scanning requires the absolute path to the Pulumi program to be supplied via policy configuration, server-side policy enforcement requires that the Pulumi program be run from a known location on disk (i.e. whatever the path on disk is that the policy is configured with in the Pulumi Cloud console) if Dockerfile scanning is desired. If <https://github.com/pulumi/pulumi-policy/issues/333> is implemented, this restriction can be lifted and the configuration value can be removed.

## Troubleshooting

### Failed to connect to Docker Daemon

If Pulumi gives the following error:

```text
Docker native provider returned an unexpected error from Configure: failed to connect to any docker daemon
```

Start Docker Desktop on your machine.

### Snyk Unable to find Docker Socket

If the Snyk CLI gives you an error similar to the following:

```text
connect ENOENT /var/run/docker.sock
```

You may need to set the `DOCKER_HOST` environment variable. At the time of writing, the Snyk CLI appears to assume that the Docker socket is running in the older (privileged) location. The newer version of Docker use a socket placed in the current user's home directory, e.g.:

```bash
export DOCKER_HOST=unix:///Users/jkodroff/.docker/run/docker.sock
```
2 changes: 2 additions & 0 deletions snyk-container-scan-policy-ts/infra/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/bin/
/node_modules/
1 change: 1 addition & 0 deletions snyk-container-scan-policy-ts/infra/AlpineDockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM alpine:3.19.1
1 change: 1 addition & 0 deletions snyk-container-scan-policy-ts/infra/DebianDockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM debian:12.5
7 changes: 7 additions & 0 deletions snyk-container-scan-policy-ts/infra/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: snyk-container-scan-policy-ts
runtime: nodejs
description: A minimal TypeScript Pulumi program
config:
pulumi:tags:
value:
pulumi:template: typescript
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
echo "$(jq --arg pwd "$(pwd)" '.["snyk-container-scan"] += {"pulumiProgramAbsPath": $pwd}' policy-config.json)" > policy-config.json
24 changes: 24 additions & 0 deletions snyk-container-scan-policy-ts/infra/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as docker from "@pulumi/docker";

// This image does not have critical issues:
new docker.Image("alpine", {
imageName: "docker.io/joshkodroff/snyk-policy-alpine",
buildOnPreview: true,
build: {
dockerfile: "AlpineDockerfile",
platform: "linux/amd64",
},
skipPush: true,
});

// This image has critical issues:
new docker.Image("debian", {
imageName: "docker.io/joshkodroff/snyk-policy-debian",
buildOnPreview: true,
build: {
dockerfile: "DebianDockerfile",
platform: "linux/amd64",
},
skipPush: true,
});

11 changes: 11 additions & 0 deletions snyk-container-scan-policy-ts/infra/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "snyk-container-scan-policy-ts",
"main": "index.ts",
"devDependencies": {
"@types/node": "^18"
},
"dependencies": {
"@pulumi/docker": "^4.5.1",
"@pulumi/pulumi": "^3.0.0"
}
}
6 changes: 6 additions & 0 deletions snyk-container-scan-policy-ts/infra/policy-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"snyk-container-scan": {
"enforcementLevel": "mandatory",
"dockerfileScanning": false
}
}
18 changes: 18 additions & 0 deletions snyk-container-scan-policy-ts/infra/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"strict": true,
"outDir": "bin",
"target": "es2016",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
},
"files": [
"index.ts"
]
}
2 changes: 2 additions & 0 deletions snyk-container-scan-policy-ts/policy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/bin/
/node_modules/
2 changes: 2 additions & 0 deletions snyk-container-scan-policy-ts/policy/PulumiPolicy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
runtime: nodejs
description: A minimal Policy Pack for AWS using TypeScript.
98 changes: 98 additions & 0 deletions snyk-container-scan-policy-ts/policy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { PolicyPack, PolicyResource, ReportViolation, StackValidationArgs } from "@pulumi/policy";
const awaitSpawn = require("await-spawn");
const fs = require("fs");

interface SnykPolicyConfig {
dockerfileScanning: boolean,
excludeBaseImageVulns: boolean,
failOn: string,
pulumiProgramAbsPath: string,
severityThreshold: string,
}

const validateStack = async (args: StackValidationArgs, reportViolation: ReportViolation) => {
const config = args.getConfig<SnykPolicyConfig>();

if (config.dockerfileScanning && !config.pulumiProgramAbsPath) {
throw new Error("If `dockerfileScanning` is configured to be `true`, `pulumiProgramAbsPath` must be set to the absolute path of the Pulumi program this policy is evaluating.");
}

const dockerImages = args.resources.filter(x => x.type === "docker:index/image:Image");
for (const image of dockerImages) {
await validateStackImage(config, image, reportViolation);
}
};

const validateStackImage = async (config: SnykPolicyConfig, image: PolicyResource, reportViolation: ReportViolation) => {
const commandArgs = [
"container",
"test",
image.props["imageName"],
];

if (config.dockerfileScanning) {
const dockerfileAbsPath = `${config.pulumiProgramAbsPath}/${image.props.dockerfile}`;

if (!fs.existsSync(dockerfileAbsPath)) {
const msg = `dockerfileScanning is set to 'true', but the Dockerfile at path '${dockerfileAbsPath}' could not be found. Either reconfigure the policy to turn off Dockerfile scanning, or set the value of docker.Image.snyk.dockerfileAbsPath resource to the absolute path of the Dockerfile in a resource transform.`;
reportViolation(msg);
return;
}

commandArgs.push(`--file=${dockerfileAbsPath}`);
}

if (config.excludeBaseImageVulns) {
commandArgs.push("--exclude-base-image-vulns");
}

commandArgs.push(`--severity-threshold=${config.severityThreshold}`);

try {
await awaitSpawn("snyk", commandArgs);
} catch (e) {
let errorMessage = `Snyk validation failed.`;

if (e.stdout && e.stdout.toString()) {
errorMessage += `\n${e.stdout.toString()}`;
}

if (e.stderr && e.stderr.toString()) {
errorMessage += `\n${e.stderr.toString()}`;
}

reportViolation(errorMessage);
}
};

new PolicyPack("snyk-container-scanning", {
policies: [{
name: "snyk-container-scan",
configSchema: {
properties: {
"dockerfileScanning": {
default: true,
type: "boolean",
},
"excludeBaseImageVulns": {
default: false,
type: "boolean"
},
"failOn": {
default: "all",
enum: ["all", "upgradable"]
},
"pulumiProgramAbsPath": {
type: "string"
},
"severityThreshold": {
default: "critical",
enum: ["low", "medium", "high", "critical"]
},
},
},
enforcementLevel: "mandatory",
description: "Scans Docker Images with Snyk",
validateStack: validateStack,
}],
});
14 changes: 14 additions & 0 deletions snyk-container-scan-policy-ts/policy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "synk-container-scan",
"version": "0.0.1",
"main": "index.ts",
"devDependencies": {
"@types/node": "^18"
},
"dependencies": {
"@pulumi/docker": "^4.5.1",
"@pulumi/policy": "^1.10.0",
"@pulumi/pulumi": "^3.0.0",
"await-spawn": "^4.0.2"
}
}
21 changes: 21 additions & 0 deletions snyk-container-scan-policy-ts/policy/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"outDir": "bin",
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"sourceMap": false,
"stripInternal": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true,
},
"files": [
"index.ts"
]
}

0 comments on commit 1c914c5

Please sign in to comment.