RedFlag/RAF/scanners/04-dnf-scanner.md
Fimeg 67e26be2d9 projection: begin the exact-path public epoch
The public tree and its history contain only the listed paths. Earlier projection history remains preserved internally.

Source-Sha: 913fde029b935671833254797f0f20f1eb9fabba

Policy-Sha: 913fde029b935671833254797f0f20f1eb9fabba

Tree-Digest: 180ae530c1058a2a5c89837bdce2d323ae83e669e38590ca72e75b8d92b7262f
2026-09-08 21:59:33 -04:00

134 lines
3.6 KiB
Markdown

# DNF Scanner
**DNF package manager scanning for Fedora/RHEL-based Linux agents.**
---
## Component Details
| Property | Value |
|----------|-------|
| Method | `dnf check-update --refresh` |
| Platform | Linux (Fedora, RHEL, CentOS) |
| Execution time | ~15 seconds per scan |
| Output format | JSON array of package objects with version info |
| Failure modes | DNF lock held, network timeout, permission denied |
---
## Implementation
**File:** `agent/internal/scanner/dnf.go`
```go
func ScanDNF(ctx context.Context) ([]DNFUpdate, error) {
// 1. Run dnf check-update --refresh
cmd := exec.CommandContext(ctx, "dnf", "check-update", "--refresh")
output, err := cmd.CombinedOutput()
if err != nil {
// Check if it's a no-update case (exit code 100)
if exitErr, ok := err.(*exec.ExitError); ok {
if exitErr.ExitCode() == 100 {
return []DNFUpdate{}, nil
}
}
logSecurityEvent("[reliability] [agent] [dnf] dnf check-update failed:", err)
return nil, err
}
// 2. Parse output
var updates []DNFUpdate
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "Last metadata expiration check") {
continue
}
// Parse "package-name.old_version.new_version"
parts := strings.Fields(line)
if len(parts) < 3 {
continue
}
pkg := parts[0]
oldVersion := parts[1]
newVersion := parts[2]
updates = append(updates, DNFUpdate{
Package: pkg,
OldVersion: oldVersion,
NewVersion: newVersion,
})
}
return updates, nil
}
```
---
## Integration Points
### 1. Command Dispatch
The DNF scanner is invoked via command execution flow:
```go
// agent/internal/orchestrator/system_scanner.go
case "scan_dnf":
updates, err := dnf.ScanDNF(ctx)
if err != nil {
return err
}
report := &SystemEvent{
AgentID: agentID,
EventType: EventTypeAgentScan,
ScanType: "dnf",
Data: updates,
}
return reportSystemEvent(report)
```
### 2. Circuit Breaker
- **Failure threshold:** 5 failures in 60 seconds
- **Open duration:** 300 seconds (5 minutes)
- **Half-open attempts:** 3 consecutive successes to recover
**Cross-references:**
- `core/01-ethos.md` (principle #3: Assume Failure)
- `verification/04-replay-protection.md` (circuit breaker integration)
---
## Systemd Integration
The DNF scanner requires two paths writable under `ProtectSystem=strict`:
- **`/var/log`** — dnf5 writes `/var/log/dnf5.log`
- **`/var/cache`** — dnf5 creates temp files at `/var/cache/libdnf5/`
**Files to update when locking down a new agent:**
- Live systemd unit: `/etc/systemd/system/redflag-agent.service` → add to `ReadWritePaths`
- Installer template: `agent/internal/installer/sudoers.go:CreateSystemdService()` → same change
**Cross-references:**
- `core/01-ethos.md` (principle #1: Errors are History — log writes must succeed)
- `agent/internal/installer/sudoers.go` (systemd service template)
---
## Footer: Assumptions & Connections
**Assumption:** DNF scanning is a periodic operation that runs independently of command execution.
**Connection:** Circuit breaker (`verification/04-replay-protection.md`) prevents DNF scanner from blocking other subsystems.
**Connection:** System events (`flows/04-heartbeat.md`) published to history table for audit trail.
**Connection:** DNF scanner (`scanners/04-dnf-scanner.md`) lives in `agent/internal/scanner/dnf.go`.
---
*Last reviewed: 2026-05-26*