pulumi / pulumi-policy

Pulumi's Policy as Code SDK, CrossGuard. Define infrastructure checks in code to enforce security, compliance, cost, and other practices, enforced at deployment time.
https://www.pulumi.com/docs/guides/crossguard/
Apache License 2.0
31 stars 4 forks source link

Properties added via a transform are not available in resources in a stack validation policy #340

Open jkodroff opened 3 months ago

jkodroff commented 3 months ago

What happened?

I have a transform which adds an arbitrary property to a resource. The added property is available in a resource policy, but not a stack policy:

Stack policy:

    image = {
      "type": "docker:index/image:Image",
      "props": {
        "baseImageName": "docker.io/joshkodroff/snyk-policy-good-image",
        "context": ".",
        "dockerfile": "GoodDockerfile",
        "imageName": "docker.io/joshkodroff/snyk-policy-good-image",
        "registryServer": "",
        "repoDigest": "sha256:b2f5dd30f96f5ba6841e6a6f26dc68cf4325e0e0dcdac0ec96a5044c69b0cfcb"
      },
      "urn": "urn:pulumi:dev::demo-pulumi-policy-snyk::docker:index/image:Image::good-image",
      "name": "good-image",
...

Resource policy:

   args = {
      "type": "docker:index/image:Image",
      "props": {
...
// added properties via transform are here:
        "snyk": {
          "dockerfileAbsPath": "/Users/jkodroff/src/jkodroff/demo-pulumi-policy-snyk/infra/GoodDockerfile"
        }
      },
    }

Example

Pulumi program:

import * as docker from "@pulumi/docker";
const resolve = require('path').resolve;

const addDockerfileAbsPath = (args: any) => {
  // TODO: Could we move this function to an export in the Policy Pack if we
  // were to publish it as an npm package like compliance ready policies?
  // That way, we could do:
  // transformations: [snykPolicy.addDockerfilePath]

  if (args.props["build"]?.dockerfile === undefined) {
    // There's no path to a Dockerfile, so there's nothing to verify
    return args;
  }

  const context = args.props["build"]?.context as string ?? ".";
  const dockerfile = args.props["build"].dockerfile as string;
  const localPath = `${context}/${dockerfile}`;
  const absPath = resolve(localPath);

  args.props["snyk"] = {};
  args.props["snyk"]["dockerfileAbsPath"] = absPath;

  return args;
};

new docker.Image("good-image", {
  imageName: "docker.io/joshkodroff/snyk-policy-good-image",
  buildOnPreview: true,
  build: {
    dockerfile: "GoodDockerfile",
    platform: "linux/arm64",
  },
}, {
  transformations: [addDockerfileAbsPath]
});

Policy:

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

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

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

const validateImage = async (config: SnykPolicyConfig, image: PolicyResource, reportViolation: ReportViolation) => {
    console.log("validateImage");
    console.log(`image = ${JSON.stringify(image, null, 2)}`);
};

const debugResource = validateResourceOfType(docker.Image, async (image, args, reportViolation) => {
    console.log("debugResource");
    console.log(`image = ${JSON.stringify(image, null, 2)}`);
    console.log(`args = ${JSON.stringify(args, null, 2)}`);
});

new PolicyPack("demo-snyk", {
    policies: [{
        configSchema: {
            properties: {
                // TODO: log level (console and pass to Snyk)
                "dockerfileScanning": {
                    default: true,
                    type: "boolean",
                },
                "excludeBaseImageVulns": {
                    default: false,
                    type: "boolean"
                },
                "failOn": {
                    default: "all",
                    enum: ["all", "upgradable"]
                },
                "severityThreshold": {
                    default: "critical",
                    enum: ["low", "medium", "high", "critical"]
                },
            },
        },
        enforcementLevel: "mandatory",
        name: "snyk-container-scan",
        description: "Scans Docker Images with Snyk",
        validateStack: validateStack,
    },
    {
        name: "debug",
        description: "debug",
        validateResource: debugResource,
    }],
});

Output of pulumi about

CLI          
Version      3.109.0
Go Version   go1.22.1
Go Compiler  gc

Plugins
NAME    VERSION
docker  4.5.1
nodejs  unknown

Host     
OS       darwin
Version  14.3.1
Arch     arm64

This project is written in nodejs: executable='/opt/homebrew/bin/node' version='v21.2.0'

Current Stack: jkodrofftest/demo-pulumi-policy-snyk/dev

Found no resources associated with dev

Found no pending operations associated with dev

Backend        
Name           pulumi.com
URL            https://app.pulumi.com/josh-pulumi-corp
User           josh-pulumi-corp
Organizations  josh-pulumi-corp, pulumi-gitlab-demo2, jkodrofftest, zephyr, pulumi
Token type     personal

Dependencies:
NAME            VERSION
@pulumi/docker  4.5.1
@pulumi/pulumi  3.105.0
@types/node     18.19.15

Pulumi locates its logs in /var/folders/5m/4n1x3f8151s35wc80w06z5k80000gn/T/ by default

Additional context

No response

Contributing

Vote on this issue by adding a 👍 reaction. To contribute a fix for this issue, leave a comment (and link to your pull request, if you've opened one already).

Frassle commented 3 months ago

Stack policies only get to see resource outputs, this would need resource inputs sent as well.

jkodroff commented 3 months ago

@Frassle Is there any reason a stack policy and a resource policy should not have the same object shape?