Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Snyk policy example #1624

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
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:
Copy link
Member

Choose a reason for hiding this comment

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

Suggest adding just a slight but of intro here that makes it clear this is an example that shows how to do this, rather than a tool for doing this, if that makes sense.


- 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
Copy link
Member

Choose a reason for hiding this comment

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

We should probably mention here:

  • Ensure the Docker deamon is running
  • npm install && npm -C ../policy install
  • pulumi stack init dev (or whatever)

```

## 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
Comment on lines +22 to +23
Copy link
Member

Choose a reason for hiding this comment

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

When I run this, I get the following error. Expected?

Previewing update (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/christian-pulumi-corp/snyk-container-scan-policy-ts/dev/previews/45a17a21-3daf-45f6-abd9-38f60d8de683

Loading policy packs...

     Type                   Name                               Plan       Info
 +   pulumi:pulumi:Stack    snyk-container-scan-policy-ts-dev  create     1 error; 2 messages
 +   ├─ docker:index:Image  alpine                             create     
 +   └─ docker:index:Image  debian                             create     1 error

Policies:
    ✅ [email protected] (local: ../policy)

Diagnostics:
  pulumi:pulumi:Stack (snyk-container-scan-policy-ts-dev):
    (node:11647) DeprecationWarning: Calling start() is no longer necessary. It can be safely omitted.
    (Use `node --trace-deprecation ...` to show where the warning was created)

    error: preview failed

  docker:index:Image (debian):
    error: Preview failed: error reading build output: failed to register layer: write /var/cache/debconf/templates.dat: no space left on device

```

- `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"
]
}
Loading