Skip to content
Open
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ mkdir -p ./artifacts
kubectl cp "${NAMESPACE}/${POD_NAME}:/artifacts/." "./artifacts/"
```

Your `./artifacts` directory will now contain `results.json`, `results.csv`, and `scan.log`.
Your `./artifacts` directory will now contain `results.json`, `results.csv`, `scan.log`, and (when enabled) `tls-posture.intoto.json`.

### Host-based scanning

Expand Down Expand Up @@ -186,6 +186,7 @@ The scanner binary accepts the following command-line options. These are configu
- `-json-file <file>` - Output results in JSON format to specified file
- `-csv-file <file>` - Output results in CSV format to specified file
- `-junit-file <file>` - Output results in JUnit XML format to specified file
- `-attestation-file <file>` - Write an unsigned TLS posture attestation JSON (per-component PASS/FAIL rollup) suitable for Cosign signing by CI. Includes optional ClusterVersion when scanning with `--all-pods`. Policy bar is `pqc` with `-pqc-check`, `tls-profile` when cluster TLS adherence is enforced, otherwise `observe`.
- `-log-file <file>` - Redirect all log output to the specified file
- `-timing-file <file>` - Write timing report to specified file in artifact-dir
- `-starttls-ports <mapping>` - Enable STARTTLS for specific ports (e.g., `postgres=5432:6432,mysql=3306`). Comma separates protocols, colon separates multiple ports within a protocol. Supported protocols: `ftp`, `smtp`, `lmtp`, `pop3`, `imap`, `xmpp`, `xmpp-server`, `telnet`, `ldap`, `nntp`, `sieve`, `postgres`, `mysql`. **Auto-detection:** When process names are available from `/proc` discovery (e.g., `postgres`, `mysqld`), STARTTLS is used automatically without needing this flag. Explicit `--starttls-ports` mappings take priority over auto-detection.
Expand Down
36 changes: 28 additions & 8 deletions cmd/tls-scanner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func run(args []string) (exitCode int) {
jsonFile := fs.String("json-file", "", "Output results in JSON format to specified file in artifact-dir")
csvFile := fs.String("csv-file", "", "Output results in CSV format to specified file in artifact-dir")
junitFile := fs.String("junit-file", "", "Output results in JUnit XML format to specified file in artifact-dir")
attestationFile := fs.String("attestation-file", "", "Write unsigned TLS posture attestation JSON (component rollup) to specified file in artifact-dir")
concurrentScans := fs.Int("j", 0, "Number of concurrent scans; 0 = runtime.NumCPU()")
allPods := fs.Bool("all-pods", false, "Scan all pods in the cluster (overrides --host)")
componentFilter := fs.String("component-filter", "", "Filter pods by a comma-separated list of component names (only used with --all-pods)")
Expand Down Expand Up @@ -176,6 +177,25 @@ func run(args []string) (exitCode int) {
var client *k8s.Client
var pods []k8s.PodInfo

writeOutputs := func(scanResults scanner.ScanResults) error {
meta := &output.AttestationMeta{
ScannerVersion: version,
ScannerCommit: commit,
}
if client != nil {
if cv, err := client.GetClusterVersionInfo(); err != nil {
slog.Debug("cluster version unavailable for attestation", "error", err)
} else {
meta.ClusterVersion = cv
}
}
return output.WriteOutputFiles(scanResults, *artifactDir, *jsonFile, *csvFile, *junitFile, *attestationFile, isPQCCheck, meta)
}

noFileOutputs := func() bool {
return *jsonFile == "" && *csvFile == "" && *junitFile == "" && *attestationFile == ""
}

if *targets != "" {
targetList := strings.Split(*targets, ",")
if len(targetList) == 0 || (len(targetList) == 1 && targetList[0] == "") {
Expand Down Expand Up @@ -206,13 +226,13 @@ func run(args []string) (exitCode int) {
scanResults := scanner.Scan(jobs, *concurrentScans, nil, tlsProfileOverride, policy, timeouts, starttlsPorts)
finalScanResults = &scanResults

if err := output.WriteOutputFiles(scanResults, *artifactDir, *jsonFile, *csvFile, *junitFile, isPQCCheck); err != nil {
if err := writeOutputs(scanResults); err != nil {
slog.Error("writing output files", "error", err)
return 1
}
if isPQCCheck {
output.PrintPQCClusterResults(scanResults)
} else if *jsonFile == "" && *csvFile == "" && *junitFile == "" {
} else if noFileOutputs() {
output.PrintClusterResults(scanResults)
}

Expand All @@ -229,13 +249,13 @@ func run(args []string) (exitCode int) {
scanResults := scanner.Scan(jobs, *concurrentScans, nil, tlsProfileOverride, policy, timeouts, starttlsPorts)
finalScanResults = &scanResults

if err := output.WriteOutputFiles(scanResults, *artifactDir, *jsonFile, *csvFile, *junitFile, isPQCCheck); err != nil {
if err := writeOutputs(scanResults); err != nil {
slog.Error("writing output files", "error", err)
return 1
}
if isPQCCheck {
output.PrintPQCClusterResults(scanResults)
} else if *jsonFile == "" && *csvFile == "" && *junitFile == "" {
} else if noFileOutputs() {
output.PrintClusterResults(scanResults)
}

Expand Down Expand Up @@ -292,13 +312,13 @@ func run(args []string) (exitCode int) {
scanResults := scanner.PerformClusterScan(pods, *concurrentScans, client, policy, timeouts, tlsProfileOverride, starttlsPorts)
finalScanResults = &scanResults

if err := output.WriteOutputFiles(scanResults, *artifactDir, *jsonFile, *csvFile, *junitFile, isPQCCheck); err != nil {
if err := writeOutputs(scanResults); err != nil {
slog.Error("writing output files", "error", err)
return 1
}
if isPQCCheck {
output.PrintPQCClusterResults(scanResults)
} else if *jsonFile == "" && *csvFile == "" && *junitFile == "" {
} else if noFileOutputs() {
output.PrintClusterResults(scanResults)
}

Expand All @@ -321,13 +341,13 @@ func run(args []string) (exitCode int) {
scanResults := scanner.Scan(jobs, *concurrentScans, client, tlsProfileOverride, policy, timeouts, starttlsPorts)
finalScanResults = &scanResults

if err := output.WriteOutputFiles(scanResults, *artifactDir, *jsonFile, *csvFile, *junitFile, isPQCCheck); err != nil {
if err := writeOutputs(scanResults); err != nil {
slog.Error("writing output files", "error", err)
return 1
}
if isPQCCheck {
output.PrintPQCClusterResults(scanResults)
} else if *jsonFile == "" && *csvFile == "" && *junitFile == "" {
} else if noFileOutputs() {
output.PrintParsedResults(scanResults)
}

Expand Down
30 changes: 30 additions & 0 deletions internal/k8s/clusterversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package k8s

import (
"context"
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ClusterVersionInfo is the OpenShift payload identity useful as an attestation subject.
type ClusterVersionInfo struct {
Version string `json:"version"`
Image string `json:"image"`
}

// GetClusterVersionInfo returns desired version and release image from clusterversion/cluster.
// Returns an error when the OpenShift config API is unavailable (e.g. plain Kubernetes).
func (c *Client) GetClusterVersionInfo() (*ClusterVersionInfo, error) {
if c == nil || c.configClient == nil {
return nil, fmt.Errorf("openshift config client not available")
}
cv, err := c.configClient.ConfigV1().ClusterVersions().Get(context.TODO(), "cluster", metav1.GetOptions{})
Comment thread
smith-xyz marked this conversation as resolved.
if err != nil {
return nil, fmt.Errorf("get clusterversion/cluster: %w", err)
}
return &ClusterVersionInfo{
Version: cv.Status.Desired.Version,
Image: cv.Status.Desired.Image,
}, nil
}
Loading