Skip to content

b2_02_application_snapshot_

Simon Baird edited this page Apr 23, 2025 · 2 revisions

Chapter 2: Application Snapshot

In Chapter 1: Policy Definition & Evaluation, we learned how ec-cli acts like a judge using a rulebook (policies) to check our software. But how does the judge know exactly what software needs checking? Does it check just one container image? Multiple images? Maybe something else?

This is where the Application Snapshot comes in.

What Problem Does the Application Snapshot Solve?

Imagine you're releasing a new version of your web application. This application isn't just one program; it might have several parts:

  • A container image for the web frontend.
  • A container image for the backend API.
  • Maybe even a container image for a database.

When you want to ensure this entire release meets your security standards, you need a way to tell ec-cli: "Please check the specific versions of the frontend image AND the backend image for this release."

Manually running ec-cli separately for each image is tedious and doesn't capture the idea of a combined release. We need a way to group these related software components together.

Our Goal: To understand how ec-cli uses the Application Snapshot to know exactly which software components (like container images) to validate for a specific application release.

Key Concepts

  1. Application Snapshot (The List):

    • Think of it as a shopping list or a manifest. It's a structured list (usually in JSON or YAML format) that details all the software pieces belonging to a specific version of your application.
    • It tells ec-cli, "Here are the items you need to validate."
  2. Components (Items on the List):

    • Each item in the snapshot list is a "component".
    • Most commonly, components are container images, identified by their specific registry URL and tag or digest (e.g., quay.io/my-org/frontend:v1.2.0).
    • Sometimes, components might also include information about their source code repository, although we'll focus on container images for now.
  3. The Input:

    • The Application Snapshot is the primary input that defines the scope of what ec-cli needs to validate.
    • ec-cli takes this list and applies the Policies to each component listed in the snapshot.

Analogy: If policy is the rulebook, the Application Snapshot is the list of players that need to be checked against that rulebook.

How to Use an Application Snapshot

Let's go back to our web application release with a frontend and a backend. We can define an Application Snapshot for it in a YAML file, maybe called my-app-v1.2.0-snapshot.yaml:

# my-app-v1.2.0-snapshot.yaml
application: my-web-app
components:
  - name: frontend-component
    containerImage: quay.io/my-org/frontend:v1.2.0
  - name: backend-component
    containerImage: quay.io/my-org/backend:v1.2.0
    # Optionally, link to source code
    # source:
    #   git:
    #     repository: https://github.com/my-org/backend
    #     revision: a1b2c3d4

This simple file lists two components:

  • frontend-component using the image quay.io/my-org/frontend:v1.2.0.
  • backend-component using the image quay.io/my-org/backend:v1.2.0.

Now, how do we tell ec-cli to use this list? We use the ec validate image command (which we saw in Chapter 1) but instead of specifying a single --image, we use the --images flag to point to our snapshot file:

# Validate all components listed in the snapshot file
ec validate image --images my-app-v1.2.0-snapshot.yaml \
                  --policy my-org/standard-policy
  • --images my-app-v1.2.0-snapshot.yaml: Tells ec-cli to read the list of components from this file.
  • --policy my-org/standard-policy: Specifies the policy rulebook to use (just like in Chapter 1).

What happens?

ec-cli will:

  1. Read my-app-v1.2.0-snapshot.yaml.
  2. Identify the two components: frontend-component and backend-component.
  3. For each component:
    • Gather information about its container image (quay.io/my-org/frontend:v1.2.0, then quay.io/my-org/backend:v1.2.0).
    • Evaluate that image against the rules defined in the my-org/standard-policy Policy.
  4. Combine the results for all components into a final report (covered in Output Reporting).

You can also provide the snapshot spec directly as a JSON string:

ec validate image --images '{"components":[{"name":"nginx","containerImage":"registry.access.redhat.com/ubi9/nginx-120:latest"}]}' \
                  --policy my-org/standard-policy

Or even just provide a single image, which ec-cli implicitly treats as a snapshot with one component:

ec validate image --image registry.access.redhat.com/ubi9/nginx-120:latest \
                  --policy my-org/standard-policy

The Application Snapshot provides a flexible way to define exactly what needs checking.

Under the Hood: How ec-cli Processes the Snapshot

When you run ec validate image --images my-app-v1.2.0-snapshot.yaml ...:

  1. Parse Input: ec-cli detects the --images flag and sees it's a file path.
  2. Read Snapshot: It reads the my-app-v1.2.0-snapshot.yaml file.
  3. Parse YAML/JSON: It parses the content into an internal data structure that represents the list of components (SnapshotSpec).
  4. Handle Image Indexes (Optional but Important!): If a containerImage points to a multi-platform image index (like an image built for both amd64 and arm64), ec-cli often expands this. It treats the index and each platform-specific image as separate components to validate, ensuring all variants are checked.
  5. Loop and Validate: The main validation logic then iterates through each component (including any expanded ones) in the internal SnapshotSpec. For each component, it fetches image data, runs the policy evaluation (as described in Chapter 1), and collects the results.

Simplified Sequence Diagram:

sequenceDiagram
    participant User
    participant CLI as ec validate image
    participant InputParser as Snapshot Input Parser
    participant ValidatorLoop as Component Validator
    participant PolicyEvaluator as Policy Engine

    User->>CLI: Run with --images snapshot.yaml --policy my-policy
    CLI->>InputParser: DetermineInputSpec(snapshot.yaml)
    InputParser-->>CLI: Return internal SnapshotSpec (list of components)
    CLI->>ValidatorLoop: Start validation for each component in SnapshotSpec
    loop For Each Component
        ValidatorLoop->>PolicyEvaluator: Evaluate(component data, policy)
        PolicyEvaluator-->>ValidatorLoop: Evaluation results (Pass/Fail/Warn)
    end
    ValidatorLoop-->>CLI: Aggregated results
    CLI->>User: Display final report
Loading

Diving into the Code (A Glimpse)

Let's see where this happens in the code.

1. Reading the Input (cmd/validate/image.go)

The validateImageCmd uses a helper function DetermineInputSpec before the main command logic runs (PreRunE) to figure out the list of components.

// Simplified concept from cmd/validate/image.go - PreRunE section
PreRunE: func(cmd *cobra.Command, args []string) (allErrors error) {
    // ... other setup ...

    // Determine the input specification based on flags like
    // --images, --image, --snapshot, --file-path, --json-input
    inputSource := applicationsnapshot.Input{
        Images:   data.images, // The primary flag now
        Image:    data.imageRef,
        Snapshot: data.snapshot,
        // Deprecated flags below:
        File:     data.filePath,
        JSON:     data.input,
    }
    spec, err := applicationsnapshot.DetermineInputSpec(ctx, inputSource)
    if err != nil {
        // Handle error if input can't be determined
        allErrors = errors.Join(allErrors, err)
    } else {
        // Store the determined SnapshotSpec for later use
        data.spec = spec
    }

    // ... policy loading (from Chapter 1) ...
    return
},

This snippet shows how DetermineInputSpec is called early to process the --images (or related) flag and prepare the SnapshotSpec (stored in data.spec).

2. Determining the Snapshot Specification (internal/applicationsnapshot/input.go)

This is where the logic lives to read files, parse JSON, handle single images, or even fetch snapshots from Kubernetes.

// Simplified concept from internal/applicationsnapshot/input.go
func DetermineInputSpec(ctx context.Context, input Input) (*app.SnapshotSpec, error) {
    var snapshot snapshot // Internal helper struct to merge specs

    // Priority: --images (file or JSON string)
    if input.Images != "" {
        content, err := readFileOrString(ctx, input.Images) // Helper to read file or treat as string
        if err != nil { return nil, err }
        specFromFile, err := readSnapshotSource(content) // Parses YAML/JSON
        if err != nil { return nil, err }
        snapshot.merge(specFromFile) // Add components to our list
    }

    // Handle single --image flag
    if input.Image != "" {
        singleImageSpec := app.SnapshotSpec{ // Create a spec with one component
            Components: []app.SnapshotComponent{{ContainerImage: input.Image}},
        }
        snapshot.merge(singleImageSpec)
    }

    // Handle --snapshot Kubernetes reference (simplified)
    if input.Snapshot != "" {
        // k8sClient := kubernetes.NewClient(...)
        // clusterSpec := k8sClient.FetchSnapshot(...)
        // snapshot.merge(clusterSpec.Spec)
        log.Warn("Kubernetes snapshot fetching simplified here")
    }

    // ... handle deprecated flags similarly ...

    if len(snapshot.Components) == 0 {
        return nil, errors.New("no components found in snapshot or image input")
    }

    // IMPORTANT: Expand image indexes (e.g., multi-arch images)
    expandImageIndex(ctx, &snapshot.SnapshotSpec)

    return &snapshot.SnapshotSpec, nil // Return the final list
}

// Simplified helper to parse YAML/JSON content
func readSnapshotSource(input []byte) (app.SnapshotSpec, error) {
	var spec app.SnapshotSpec
	// Use sigs.k8s.io/yaml which handles both JSON and YAML
	err := yaml.Unmarshal(input, &spec)
	if err != nil {
		return app.SnapshotSpec{}, fmt.Errorf("unable to parse Snapshot spec: %w", err)
	}
	log.Debugf("Parsed Snapshot spec with %d components", len(spec.Components))
	return spec, nil
}

This shows the core logic: trying different input sources (--images, --image, etc.), parsing the YAML/JSON content using readSnapshotSource, merging components into a single list, and finally expanding image indexes before returning the final SnapshotSpec.

3. Using the Snapshot during Validation (internal/evaluation_target/application_snapshot_image/application_snapshot_image.go)

When ec-cli processes each component, it keeps track of the original component definition and the overall snapshot spec.

// Simplified concept from internal/evaluation_target/application_snapshot_image/application_snapshot_image.go
type ApplicationSnapshotImage struct {
    // ... other fields like reference, checkOpts, signatures ...

    // Holds the specific component definition from the snapshot
	component app.SnapshotComponent

    // Holds the entire snapshot spec (useful for context)
	snapshot  app.SnapshotSpec
}

// Constructor keeps track of the component and snapshot
func NewApplicationSnapshotImage(ctx context.Context, component app.SnapshotComponent, p policy.Policy, snap app.SnapshotSpec) (*ApplicationSnapshotImage, error) {
    // ... setup ...
	a := &ApplicationSnapshotImage{
		// ... other fields ...
		component: component, // Store the component being processed
		snapshot:  snap,      // Store the whole snapshot context
	}
    // ... set image URL, etc. ...
	return a, nil
}

// Preparing data for policy evaluation includes snapshot info
func (a *ApplicationSnapshotImage) WriteInputFile(ctx context.Context) (string, []byte, error) {
    // ... gather attestations, signatures, image config ...
	input := Input{
		// ... attestations, image info ...
		AppSnapshot: a.snapshot, // Include the snapshot spec in the input JSON
	}
    // ... write input JSON to a temporary file ...
    inputJSON, err := json.Marshal(input)
    // ... error handling ...
	return inputJSONPath, inputJSON, nil
}

This shows that the ApplicationSnapshotImage struct holds references to both the specific component being validated and the overall snapshot spec. This context is then included in the data passed to the policy engine (as seen in WriteInputFile), allowing policies to potentially make decisions based on the application context, not just the single image.

Conclusion

The Application Snapshot is the crucial input that tells ec-cli what to validate. It's a manifest, often a YAML or JSON file, listing the specific container images (and potentially other components) that make up a particular release of your application.

By using the --images flag with ec validate image, you provide this list to ec-cli. It then iterates through each component, applying the Policies we defined in Chapter 1.

Now that we know how to define rules (Policies) and what software to check (Application Snapshot), the next logical step is to understand how ec-cli tells us the results of its checks.

Let's move on to Chapter 3: Output Reporting to see how ec-cli presents the validation outcomes.


Generated by AI Codebase Knowledge Builder

Clone this wiki locally