Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
84b000a
Add CO->ACS scheduled scan importer tool
guzalv Mar 24, 2026
5305bb5
fix(co-importer): correct CO resource field paths in parser
guzalv Mar 24, 2026
c484ac9
fix(co-importer): correct CO resource field paths in specs
guzalv Mar 25, 2026
6bb0acd
feat(co-importer): align CLI flags and env vars with roxctl conventio…
guzalv Mar 25, 2026
5ddea6c
feat(co-importer): add spec coverage check script
guzalv Mar 25, 2026
63d9eab
feat(co-importer): add e2e test framework for real-cluster testing
guzalv Mar 25, 2026
34dae17
feat(co-importer): simplify cluster access to kubectl-native model (S…
guzalv Mar 26, 2026
2f70035
fix(co-importer): show TLS-specific hints for self-signed cert errors…
guzalv Mar 26, 2026
7a9b5c7
docs(co-importer): mark Slice I done, add Slice J (container image) t…
guzalv Mar 26, 2026
34fad87
docs(co-importer): rewrite README with usage, flags, and mapping refe…
guzalv Mar 26, 2026
11e5932
fix(co-importer): show per-method errors when cluster ID discovery fails
guzalv Mar 26, 2026
2d4a60e
feat(co-importer): load kubeconfig files independently to avoid crede…
guzalv Mar 26, 2026
35d45f4
fix(co-importer): print merge conflict warnings to console (IMP-MAP-0…
guzalv Mar 26, 2026
fee3301
fix(co-importer): demo conflict scenario uses k8s-side drift not ACS-…
guzalv Mar 26, 2026
f630977
fix(co-importer): demo drift edits ACS-managed ScanSetting on cluster
guzalv Mar 26, 2026
b91157e
fix(co-importer): use DaysOfWeek instead of Weekly to match ACS proto…
guzalv Mar 26, 2026
9a532e6
fix(co-importer): include response body in ACS HTTP error messages
guzalv Mar 26, 2026
9b4327d
feat(co-importer): adopt SSBs after ACS scan config creation (IMP-ADO…
guzalv Mar 26, 2026
7b911ca
feat(co-importer): add container image build (IMP-IMG-001..006)
guzalv Mar 26, 2026
026e184
feat(co-importer): add demo seed script for quick cluster setup
guzalv Mar 26, 2026
167729a
fix(co-importer): show error details and mapping warnings in console …
guzalv Mar 26, 2026
199c0a6
feat(co-importer): add run.sh wrapper for easy container usage
guzalv Mar 26, 2026
e0dea1e
fix(co-importer): skip adoption when target ScanSetting pre-exists on…
guzalv Mar 26, 2026
cb8cc85
fix(co-importer): clean stale manifest before creating new one
guzalv Mar 26, 2026
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
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ linters:
path: roxctl/common/io/io\.go # io.go will by default use os.Stdin/os.StdErr.
paths:
- pkg/complianceoperator/api
- scripts/compliance-operator-importer
- third_party$
- builtin$
- examples$
Expand All @@ -223,6 +224,7 @@ formatters:
generated: lax
paths:
- pkg/complianceoperator/api
- scripts/compliance-operator-importer
- third_party$
- builtin$
- examples$
6 changes: 6 additions & 0 deletions go.work
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
go 1.25.0

use (
.
./scripts/compliance-operator-importer
)
570 changes: 570 additions & 0 deletions go.work.sum

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions scripts/compliance-operator-importer/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!compliance-operator-importer
2 changes: 2 additions & 0 deletions scripts/compliance-operator-importer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin/
/compliance-operator-importer
56 changes: 56 additions & 0 deletions scripts/compliance-operator-importer/DECISIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# V1 Scope Freeze: CO -> ACS Importer

## Status

This document freezes Phase 1 behavior. Any deviation requires updating this file and corresponding specs.

## Frozen decisions

1. **Execution model**
- Standalone external importer only.
- No runtime/product code changes in Sensor/Central/ACS backend.

2. **Importer mode**
- Phase 1 is create-only.
- Importer may create new ACS scan configs.
- Importer must never update existing ACS scan configs.

3. **Implementation language**
- Use **Go** for Phase 1 implementation.
- Do not implement Phase 1 importer in bash/shell.
- Python is an acceptable future alternative only if explicitly re-decided in this file.

4. **Existing-name behavior**
- If `scanName` already exists in ACS, skip resource.
- Add one entry to `problems[]` with clear `description` and `fixHint`.

5. **Error handling model**
- Resource-level issue => skip resource, continue processing, emit `problems[]` entry.
- Fatal preflight/config issue => fail run before resource processing.

6. **Cluster targeting**
- Source cluster selected like `kubectl` (current context by default, optional context override).
- Single destination ACS cluster ID via `--acs-cluster-id`.

7. **ACS authentication model**
- Default auth mode is token (`ACS_API_TOKEN` via `--acs-token-env`).
- Optional basic-auth mode is allowed for local/dev environments.
- Basic mode uses username/password inputs and the same preflight endpoint checks.

8. **Profile kind fallback**
- Missing `ScanSettingBinding.profiles[].kind` defaults to `Profile` (profiles is a top-level field, not under spec).

9. **Schedule conversion**
- Convert valid CO cron to ACS schedule fields.
- Conversion failure => skip resource + `problems[]` entry with remediation hint.

10. **Provenance marker**

- Not required in Phase 1 create-only mode.
- Can be revisited in a future update/reconcile phase.

## Deferred to Phase 2 (out of scope)

- Update/reconcile mode (`PUT`) for existing configs.
- Ownership/provenance-based update guard.
- Multi-target cluster mapping per binding.
10 changes: 10 additions & 0 deletions scripts/compliance-operator-importer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM registry.access.redhat.com/ubi9-micro:latest

LABEL org.opencontainers.image.title="co-acs-importer" \
org.opencontainers.image.description="Compliance Operator to ACS scan configuration importer" \
org.opencontainers.image.source="https://github.com/stackrox/stackrox"

COPY compliance-operator-importer /compliance-operator-importer

USER 65534:65534
ENTRYPOINT ["/compliance-operator-importer"]
70 changes: 70 additions & 0 deletions scripts/compliance-operator-importer/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
BINARY := compliance-operator-importer
MODULE := ./cmd/importer
IMAGE ?= localhost/compliance-operator-importer
TAG ?= latest
ARCHS ?= amd64 arm64
GOARCH ?= $(shell go env GOARCH)
CONTAINER ?= $(shell command -v podman 2>/dev/null || echo docker)

.PHONY: build test image image-push image-multiarch clean \
demo-seed demo-seed-down demo-seed-status demo help test-v

## ── Build ──────────────────────────────────────────────────────────────────

build: ## Build the importer binary
CGO_ENABLED=0 go build -o $(BINARY) $(MODULE)

## ── Test ───────────────────────────────────────────────────────────────────

test: ## Run all unit tests
go test ./...

test-v: ## Run all unit tests (verbose)
go test -v ./...

## ── Container image ────────────────────────────────────────────────────────

image: ## Build container image for host arch
CGO_ENABLED=0 GOOS=linux GOARCH=$(GOARCH) go build -o $(BINARY) $(MODULE)
$(CONTAINER) build -t $(IMAGE):$(TAG) .

image-multiarch: ## Build per-arch images (use before image-push)
@for arch in $(ARCHS); do \
echo "── Building $(IMAGE):$(TAG)-$$arch ──"; \
CGO_ENABLED=0 GOOS=linux GOARCH=$$arch go build -o $(BINARY) $(MODULE) && \
$(CONTAINER) build --platform linux/$$arch -t $(IMAGE):$(TAG)-$$arch . ; \
done

image-push: image-multiarch ## Push multi-arch images and create manifest
@for arch in $(ARCHS); do \
$(CONTAINER) push $(IMAGE):$(TAG)-$$arch ; \
done
$(CONTAINER) manifest rm $(IMAGE):$(TAG) 2>/dev/null || true
$(CONTAINER) manifest create $(IMAGE):$(TAG) \
$(foreach arch,$(ARCHS),$(IMAGE):$(TAG)-$(arch))
$(CONTAINER) manifest push $(IMAGE):$(TAG)
@echo ""
@echo "Pushed: $(IMAGE):$(TAG)"

## ── Demo ───────────────────────────────────────────────────────────────────

demo-seed: ## Seed demo fixtures (2 ACS scans + 3 SSBs, 1 conflicting)
./hack/demo-seed.sh up

demo-seed-down: ## Remove demo fixtures
./hack/demo-seed.sh down

demo-seed-status: ## Show demo fixture state
./hack/demo-seed.sh status

demo: build ## Run interactive demo
./hack/demo.sh

## ── Misc ───────────────────────────────────────────────────────────────────

clean: ## Remove build artifacts
rm -f $(BINARY)

help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
152 changes: 152 additions & 0 deletions scripts/compliance-operator-importer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# CO → ACS Scheduled Scan Importer

Reads Compliance Operator `ScanSettingBinding` resources from one or more
Kubernetes clusters and creates equivalent scan configurations in Red Hat
Advanced Cluster Security (ACS) via the v2 API.

## Quick start

```bash
# Build
go build -o compliance-operator-importer ./cmd/importer

# Dry run (preview, no changes)
ROX_API_TOKEN=<token> ./compliance-operator-importer \
--endpoint central.example.com \
--dry-run

# Import for real
ROX_API_TOKEN=<token> ./compliance-operator-importer \
--endpoint central.example.com
```

## Authentication

Auth mode is auto-inferred from environment variables:

| Variable | Mode | Typical use |
|----------|------|-------------|
| `ROX_API_TOKEN` | API token (Bearer) | Production |
| `ROX_ADMIN_PASSWORD` | Basic auth | Development/testing |

Setting both is an error. Setting neither is an error.

For basic auth the username defaults to `admin`; override with `--username`
or `ROX_ADMIN_USER`.

## Multi-cluster

By default all contexts in the merged kubeconfig are processed. Merge
multiple kubeconfig files via the standard `KUBECONFIG` variable:

```bash
KUBECONFIG=cluster-a.yaml:cluster-b.yaml ./compliance-operator-importer --endpoint central.example.com
```

Use `--context` (repeatable) to limit processing to specific contexts:

```bash
./compliance-operator-importer --endpoint central.example.com \
--context prod-east \
--context prod-west
```

When the same `ScanSettingBinding` name appears across multiple clusters,
the importer merges them into a single ACS scan configuration targeting all
matched clusters (profiles and schedules must match).

## Cluster ID auto-discovery

The ACS cluster ID for each context is auto-discovered using the first
successful method:

1. `admission-control` ConfigMap → `cluster-id` key (namespace: `stackrox`)
2. OpenShift `ClusterVersion` `spec.clusterID` → matched against ACS provider metadata
3. `helm-effective-cluster-name` Secret → matched against ACS cluster name

## Flags

| Flag | Default | Description |
|------|---------|-------------|
| `--endpoint` | `ROX_ENDPOINT` | ACS Central URL (bare hostnames get `https://` prepended) |
| `--username` | `admin` | Basic auth username (`ROX_ADMIN_USER`) |
| `--context` | all | Kubeconfig context to process (repeatable) |
| `--co-namespace` | `openshift-compliance` | Namespace for CO resources |
| `--co-all-namespaces` | `false` | Read CO resources from all namespaces |
| `--dry-run` | `false` | Preview actions without changes |
| `--overwrite-existing` | `false` | Update existing ACS configs instead of skipping |
| `--report-json` | — | Write structured JSON report to file |
| `--max-retries` | `5` | Retry attempts for transient API errors (429, 502–504) |
| `--request-timeout` | `30s` | Per-request HTTP timeout |
| `--ca-cert-file` | — | PEM CA bundle for TLS |
| `--insecure-skip-verify` | `false` | Skip TLS verification |

## Behaviour

- **Create-only (default):** existing ACS scan configs with the same name
are skipped with a warning.
- **Overwrite mode** (`--overwrite-existing`): existing configs are updated
via PUT to match the cluster SSBs.
- **Idempotent:** re-running produces the same result; no duplicates.
- **Dry run:** all discovery and mapping runs normally; no POST/PUT issued.

## Exit codes

| Code | Meaning |
|------|---------|
| `0` | All bindings processed (or nothing to do) |
| `1` | Fatal error (config, auth, connectivity) |
| `2` | Partial success (some bindings failed; see report) |

## Mapping rules

Each `ScanSettingBinding` maps to one ACS scan configuration:

| ACS field | Source |
|-----------|--------|
| `scanName` | `ScanSettingBinding.metadata.name` |
| `profiles` | Sorted, deduplicated profile names from the binding |
| `scanSchedule` | Converted from the referenced `ScanSetting.schedule` (cron) |
| `clusters` | Auto-discovered ACS cluster ID(s) |
| `description` | `"Imported from CO ScanSettingBinding <ns>/<name> (ScanSetting: <ss>)"` |

Supported cron patterns: daily (`M H * * *`), weekly (`M H * * DOW`),
monthly (`M H DOM * *`). Step and range notation are not supported.

## JSON report

When `--report-json` is set, a structured report is written:

```json
{
"meta": { "timestamp": "...", "dryRun": false, "mode": "create-only" },
"counts": { "discovered": 3, "create": 2, "update": 0, "skip": 1, "failed": 0 },
"items": [ { "source": {...}, "action": "create", ... } ],
"problems": []
}
```

## Demo / testing

Seed demo fixtures (2 ACS scans + 3 SSBs, 1 conflicting):

```bash
ROX_ADMIN_PASSWORD=admin ROX_ENDPOINT=central.example.com ./hack/demo-seed.sh up
./hack/demo-seed.sh status
./hack/demo-seed.sh down
```

Interactive walkthrough:

```bash
ROX_ADMIN_PASSWORD=admin ROX_ENDPOINT=central.example.com ./hack/demo.sh
```

## Development

Specs live in `specs/` and are the source of truth. Tests reference spec IDs
(`IMP-*`). Run tests:

```bash
go test ./...
```
71 changes: 71 additions & 0 deletions scripts/compliance-operator-importer/cmd/importer/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Binary co-acs-scan-importer reads Compliance Operator ScanSettingBinding
// resources from Kubernetes clusters and creates equivalent ACS compliance
// scan configurations through the ACS v2 API.
//
// Run with --help for full usage information and examples.
package main

import (
"context"
"fmt"
"os"

"github.com/stackrox/co-acs-importer/internal/acs"
"github.com/stackrox/co-acs-importer/internal/config"
"github.com/stackrox/co-acs-importer/internal/preflight"
"github.com/stackrox/co-acs-importer/internal/run"
"github.com/stackrox/co-acs-importer/internal/status"
)

func main() {
os.Exit(mainWithCode())
}

func mainWithCode() int {
cfg, err := config.ParseAndValidate(os.Args[1:])
if err != nil {
if err == config.ErrHelpRequested {
return 0
}
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
return run.ExitFatalError
}

s := status.New()
ctx := context.Background()

// Preflight check before any resource processing.
s.Stage("Preflight", "checking ACS connectivity and credentials")
if err := preflight.Run(ctx, cfg); err != nil {
s.Failf("%v", err)
return run.ExitFatalError
}
s.OKf("ACS endpoint is reachable at %s", cfg.ACSEndpoint)

acsClient, err := acs.NewClient(cfg)
if err != nil {
s.Failf("failed to create ACS client: %v", err)
return run.ExitFatalError
}

// Build cluster sources from kubeconfig contexts.
if len(cfg.Contexts) > 0 {
s.Stagef("Discovery", "resolving %d specified contexts", len(cfg.Contexts))
} else {
s.Stage("Discovery", "resolving all kubeconfig contexts")
}
sources, err := run.BuildClusterSources(ctx, cfg, acsClient)
if err != nil {
s.Failf("%v", err)
return run.ExitFatalError
}
for _, src := range sources {
s.OKf("%s → %s", src.Label, src.ACSClusterID)
}

if len(sources) == 1 {
cfg.ACSClusterID = sources[0].ACSClusterID
return run.NewRunner(cfg, acsClient, sources[0].COClient).Run(ctx)
}
return run.NewRunner(cfg, acsClient, nil).RunMultiCluster(ctx, sources)
}
3 changes: 3 additions & 0 deletions scripts/compliance-operator-importer/e2e/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package e2e contains end-to-end acceptance tests that run against a real
// ACS + Compliance Operator cluster. Tests require the "e2e" build tag.
package e2e
Loading
Loading