Skip to content

Add support for importing FAA rules#13

Merged
russellhancox merged 4 commits into
mainfrom
rah/snt-200-add-faa-policy-import-to-santa-rule-importer
Mar 27, 2026
Merged

Add support for importing FAA rules#13
russellhancox merged 4 commits into
mainfrom
rah/snt-200-add-faa-policy-import-to-santa-rule-importer

Conversation

@russellhancox

Copy link
Copy Markdown
Member

Adds support for importing file access authorization rules, either from a separate policy plist or embedded in a Santa configuration mobileconfig. To continue allowing only StaticRules or only FAA rule imports, 2 new flags are added.

@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds import support for Santa File Access Authorization (FAA) policies (.plist and .mobileconfig): new internal FAA parser, CLI flags to select FAA vs static rules, conversion to Workshop FileAccessRule objects, per-rule creation via the Workshop API, and tests/fixtures validating mappings and defaults.

Changes

Cohort / File(s) Summary
Documentation
README.md
Expanded README to document FAA .plist/.mobileconfig imports, mapping rules (path literal vs prefix, AuditOnlyBlockViolations, AllowReadAccess defaulting, process signing ID formatting), updated CLI usage and examples, and added FAA-specific flags.
CLI Integration
cmd/main.go
Added --faa-only and --static-rules-only flags with mutual-exclusion check; extended dispatch to call FAA parsers for .plist and .mobileconfig; skip static rules when --faa-only; parse FAA rules into faaRules and create per-rule Workshop entries with Tag="global", tracking separate success/error reporting.
FAA Rule Parser
internal/faarules/faarules.go
New package to decode FAA plists/mobileconfigs and convert WatchItems into apipb.FileAccessRule slices; implements PathEntry → PathLiterals/PathPrefixes, ProcessEntry → signing/team/CDhash/cert/binary transforms (including platform: prefix when PlatformBinary), rule-type mapping, defaulting (AllowReadAccess=true when unset, BlockViolations = !AuditOnly when AuditOnly present), deterministic grouping, and error wrapping.
FAA Tests
internal/faarules/faarules_test.go
New test suite asserting parsing behavior, rule counts, field mappings, defaulting semantics, process identifier transformations (including TeamID:SigningID and platform:<SigningID>), mobileconfig payload extraction, and behavior when no FileAccessPolicy is present.
FAA Test Fixtures
internal/faarules/testdata/*
Added faa_policy.plist and santa_with_faa.mobileconfig fixtures covering multiple watch items, path matchers (literals vs prefixes), process filters, and options used by tests.
Dependencies / Toolchain
go.mod
Bumped Go directive to 1.24.0 and updated protobuf/grpc and several golang.org/x/* indirect dependencies. Updated supported input formats messaging to include .plist.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as CLI (cmd/main.go)
    participant Parser as FAA Parser (internal/faarules)
    participant API as Workshop API (gRPC)

    User->>CLI: Run tool with input file and flags
    CLI->>CLI: Validate flags (mutual exclusion)
    alt .plist input
        CLI->>Parser: ParseRulesFromFile(filePath)
    else .mobileconfig input
        CLI->>Parser: ParseRulesFromMobileConfig(filePath)
    end
    Parser->>Parser: Decode plist/mobileconfig and extract FileAccessPolicy/WatchItems
    Parser->>Parser: For each WatchItem:
    Parser->>Parser: • Split Paths into literals vs prefixes
    Parser->>Parser: • Build process filters (SigningID, TeamID, CDHash, CertSha256, BinaryPath)
    Parser->>Parser: • Apply PlatformBinary → prefix "platform:"
    Parser->>Parser: • Apply defaults and invert AuditOnly → BlockViolations
    Parser-->>CLI: Return []*apipb.FileAccessRule
    CLI->>CLI: Tag rules as "global"
    loop per FAA rule
        CLI->>API: CreateFileAccessRule(rule)
        API-->>CLI: Success/Error
    end
    CLI-->>User: Report created rules and any errors
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding support for importing File Access Authorization (FAA) rules, which is the primary objective across all modified files.
Description check ✅ Passed The description is directly related to the changeset, explaining that FAA rule import support is added via plist and mobileconfig files, with new flags for granular control over import types.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rah/snt-200-add-faa-policy-import-to-santa-rule-importer

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

32-57: ⚠️ Potential issue | 🟡 Minor

Refresh the pasted --help output.

This block still advertises only TOML/CSV inputs and omits --faa-only / --static-rules-only, so the README no longer matches the actual CLI after this PR. Please regenerate it from current ./santa-rule-importer --help.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 32 - 57, The README's pasted output of
./santa-rule-importer --help is outdated and omits new CLI flags (--faa-only and
--static-rules-only) and updated accepted input types; regenerate and paste the
current help text by running ./santa-rule-importer --help and replacing the
block under the example usage in README.md so it exactly matches the live output
(ensure the tool name santa-rule-importer and flags like --faa-only,
--static-rules-only, --use-custom-msg-as-comment, --zentral-config-id,
--zentral-target-identifier, --zentral-target-type, --zentral-url, and any
updated input type descriptions appear verbatim).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/main.go`:
- Around line 118-120: The code discards the error from
faarules.ParseRulesFromMobileConfig; change the call to capture the error (e.g.,
faaRules, parseErr := faarules.ParseRulesFromMobileConfig(filename)) and
propagate or handle it instead of ignoring it: when parseErr != nil return or
exit non‑zero with a clear error (or append to ruleSrcErr) so that failures
parsing the .mobileconfig are not silently ignored; update the logic that checks
!*staticRulesOnly and ruleSrcErr to incorporate parseErr.

In `@internal/faarules/faarules.go`:
- Around line 100-109: The current implementation in faarules.go only inspects
config.PayloadContent[0] and returns nil if the first payload lacks a
FileAccessPolicy, which skips FAA rules in multi-payload profiles; update the
logic to iterate over all entries in config.PayloadContent, find the first
non-nil FileAccessPolicy, assign it to policy and call convertWatchItems(policy)
(returning its result), and only return nil,nil if no payload contains a
FileAccessPolicy; keep references to convertWatchItems, config.PayloadContent,
and the policy variable to locate where to change the code.
- Around line 149-156: The loop over item.Processes currently logs a warning
then returns nil, nil when PlatformBinary is true but SigningID is empty, which
silently drops the watch item; change that to return a non-nil error (e.g.,
fmt.Errorf("watch item %q: PlatformBinary set but SigningID is empty", name))
from the enclosing function instead of returning nil, nil so the caller fails
fast; update the function that builds signingIDs (references: item.Processes,
proc.PlatformBinary, proc.SigningID, signingIDs, name) to propagate this error,
and remove the unused log import if it becomes unused.

---

Outside diff comments:
In `@README.md`:
- Around line 32-57: The README's pasted output of ./santa-rule-importer --help
is outdated and omits new CLI flags (--faa-only and --static-rules-only) and
updated accepted input types; regenerate and paste the current help text by
running ./santa-rule-importer --help and replacing the block under the example
usage in README.md so it exactly matches the live output (ensure the tool name
santa-rule-importer and flags like --faa-only, --static-rules-only,
--use-custom-msg-as-comment, --zentral-config-id, --zentral-target-identifier,
--zentral-target-type, --zentral-url, and any updated input type descriptions
appear verbatim).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 635127ca-9666-4bf5-9b81-08b0d0e1bfe6

📥 Commits

Reviewing files that changed from the base of the PR and between f980591 and c6c1410.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • README.md
  • cmd/main.go
  • go.mod
  • internal/faarules/faarules.go
  • internal/faarules/faarules_test.go
  • internal/faarules/testdata/faa_policy.plist
  • internal/faarules/testdata/santa_with_faa.mobileconfig

Comment thread cmd/main.go
Comment thread internal/faarules/faarules.go Outdated
Comment thread internal/faarules/faarules.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
internal/faarules/faarules.go (2)

100-109: ⚠️ Potential issue | 🟠 Major

Only the first payload is inspected; FAA rules in subsequent payloads will be silently ignored.

Multi-payload mobileconfig profiles may have the Santa configuration at an index other than 0. The current logic only checks PayloadContent[0], which could cause FAA rules to be missed.

Suggested fix: iterate all payloads
 	if len(config.PayloadContent) == 0 {
 		return nil, nil
 	}
 
-	policy := config.PayloadContent[0].FileAccessPolicy
-	if policy == nil {
-		return nil, nil
+	for _, payload := range config.PayloadContent {
+		if payload.FileAccessPolicy != nil {
+			return convertWatchItems(payload.FileAccessPolicy)
+		}
 	}
 
-	return convertWatchItems(policy)
+	return nil, nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/faarules/faarules.go` around lines 100 - 109, The code only inspects
PayloadContent[0] and ignores FAA rules in later payloads; change the logic to
iterate over all entries in config.PayloadContent, check each entry's
FileAccessPolicy, call convertWatchItems(policy) for each non-nil policy, and
aggregate the results (skip nil returns) into a single combined return value;
ensure you continue on nil policies and return the merged slice/result instead
of only the first payload's output (referencing config.PayloadContent,
FileAccessPolicy, and convertWatchItems).

149-156: ⚠️ Potential issue | 🟠 Major

Silent drop of watch item on malformed PlatformBinary input.

When PlatformBinary is true but SigningID is empty, the function logs a warning and returns nil, nil, silently omitting the rule. For policy data, a hard error is safer to avoid reporting a successful import with missing coverage.

Suggested fix: return an error instead of silently skipping
 	for _, proc := range item.Processes {
 		if proc.PlatformBinary != nil && *proc.PlatformBinary {
 			if proc.SigningID != "" {
 				signingIDs = append(signingIDs, "platform:"+proc.SigningID)
-			} else {
-				log.Printf("Warning: skipping watch item %q: PlatformBinary is true but no SigningID is set", name)
-				return nil, nil
+			} else {
+				return nil, fmt.Errorf("PlatformBinary is true but no SigningID is set")
 			}
 			continue
 		}

If this change is applied, the log import can be removed if it becomes unused.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/faarules/faarules.go` around lines 149 - 156, The loop in
faarules.go silently drops a watch item when a Process has PlatformBinary ==
true but empty SigningID (it currently calls log.Printf and returns nil, nil);
change this to return a proper error instead of returning nil, nil so the caller
sees the failure (e.g., construct and return an error mentioning the watch item
name and the missing SigningID when encountering proc.PlatformBinary &&
proc.SigningID == ""); update the function's error path accordingly and remove
the log import if it becomes unused.
🧹 Nitpick comments (1)
internal/faarules/faarules.go (1)

117-126: Map iteration yields non-deterministic rule ordering.

Iterating over policy.WatchItems (a map) produces rules in an unpredictable order. If consistent ordering is desired for reproducibility or testing, consider sorting the keys first.

Optional fix: sort keys for deterministic output
+	import "sort"
+
 func convertWatchItems(policy *FAAPolicy) ([]*apipb.FileAccessRule, error) {
 	if len(policy.WatchItems) == 0 {
 		return []*apipb.FileAccessRule{}, nil
 	}
 
+	// Sort keys for deterministic output order
+	names := make([]string, 0, len(policy.WatchItems))
+	for name := range policy.WatchItems {
+		names = append(names, name)
+	}
+	sort.Strings(names)
+
 	var rules []*apipb.FileAccessRule
-	for name, item := range policy.WatchItems {
+	for _, name := range names {
+		item := policy.WatchItems[name]
 		rule, err := convertWatchItem(name, &item, policy)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/faarules/faarules.go` around lines 117 - 126, The loop over
policy.WatchItems produces non-deterministic rules because map iteration is
unordered; to fix, collect and sort the keys of policy.WatchItems before
iterating, then call convertWatchItem(name, &item, policy) in that deterministic
key order and append to the rules slice (variable rules) as before so tests and
outputs are reproducible.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/faarules/faarules.go`:
- Around line 224-237: getRuleType currently maps strings to
apipb.FileAccessRuleType and silently returns FILE_ACCESS_RULE_TYPE_UNSPECIFIED
for unknown values; change it to either emit a warning log or return an error so
typos are surfaced. Update getRuleType to return (apipb.FileAccessRuleType,
error) (or keep the same return but add a logger call) and update all callers of
getRuleType to handle the error/inspect the log; reference the getRuleType
function and any call sites that consume its result so unrecognized inputs
produce a descriptive error or warning mentioning the original string.

---

Duplicate comments:
In `@internal/faarules/faarules.go`:
- Around line 100-109: The code only inspects PayloadContent[0] and ignores FAA
rules in later payloads; change the logic to iterate over all entries in
config.PayloadContent, check each entry's FileAccessPolicy, call
convertWatchItems(policy) for each non-nil policy, and aggregate the results
(skip nil returns) into a single combined return value; ensure you continue on
nil policies and return the merged slice/result instead of only the first
payload's output (referencing config.PayloadContent, FileAccessPolicy, and
convertWatchItems).
- Around line 149-156: The loop in faarules.go silently drops a watch item when
a Process has PlatformBinary == true but empty SigningID (it currently calls
log.Printf and returns nil, nil); change this to return a proper error instead
of returning nil, nil so the caller sees the failure (e.g., construct and return
an error mentioning the watch item name and the missing SigningID when
encountering proc.PlatformBinary && proc.SigningID == ""); update the function's
error path accordingly and remove the log import if it becomes unused.

---

Nitpick comments:
In `@internal/faarules/faarules.go`:
- Around line 117-126: The loop over policy.WatchItems produces
non-deterministic rules because map iteration is unordered; to fix, collect and
sort the keys of policy.WatchItems before iterating, then call
convertWatchItem(name, &item, policy) in that deterministic key order and append
to the rules slice (variable rules) as before so tests and outputs are
reproducible.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eec93281-949f-42f4-9a6d-840c40a3ea03

📥 Commits

Reviewing files that changed from the base of the PR and between c6c1410 and ee529b4.

📒 Files selected for processing (1)
  • internal/faarules/faarules.go

Comment thread internal/faarules/faarules.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/main.go`:
- Around line 52-53: The flag validation is too lax: the boolean flags faaOnly
and staticRulesOnly must be validated against the selected input file type
before any import runs; add a guard in main (before entering the
mobileconfig/CSV import branches) that examines the input filename/format and
rejects incompatible combinations (e.g., --static-rules-only with a
.mobileconfig must error if the chosen import path would still process FAA
rules, and --faa-only with non-mobileconfig/CSV must error similarly). Locate
the argument parsing where faaOnly and staticRulesOnly are defined and enforce
the check right after parsing and before calling the mobileconfig import branch
(the code section that handles the ".mobileconfig" branch and the CSV/static
import branches), returning a non-zero exit and a clear error message when a
flag/file combination cannot honor the request.
- Around line 181-188: The loop that calls client.CreateFileAccessRule currently
uses context.Background(), which can hang; change it to create a per-RPC context
with a timeout (e.g., using context.WithTimeout) and pass that to
client.CreateFileAccessRule, ensuring you call the returned cancel() (defer or
immediate) to release resources; update the code around faaReq, faaRules and the
call site client.CreateFileAccessRule to use the new timed context so each FAA
write is bounded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a0c5ccf7-9b25-4530-ba22-b31155da186c

📥 Commits

Reviewing files that changed from the base of the PR and between ee529b4 and 20bd606.

📒 Files selected for processing (3)
  • README.md
  • cmd/main.go
  • internal/faarules/faarules.go
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/faarules/faarules.go

Comment thread cmd/main.go
Comment thread cmd/main.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/faarules/faarules.go (1)

157-165: Add comment clarifying PlatformBinary-only identification.

The continue statement on line 164 is intentional: when PlatformBinary is true, the process is identified solely by its signing ID (prefixed with "platform:"), and other fields (CDHash, CertificateSha256, BinaryPath) are ignored. This is confirmed by the test expectation at line 92 of faarules_test.go.

Add an inline comment before or after the continue statement to explain this behavior, since it silently ignores other identifier fields on the same ProcessEntry:

// Platform binaries are identified only by signing ID
continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/faarules/faarules.go` around lines 157 - 165, Add an inline comment
explaining that when a ProcessEntry has PlatformBinary == true the process is
identified solely by SigningID (prefixed with "platform:") and other identifier
fields (CDHash, CertificateSha256, BinaryPath) are intentionally ignored; place
this comment immediately before or after the existing continue in the loop that
iterates item.Processes so it's clear why the code returns early in that branch
(referencing PlatformBinary, SigningID, CDHash, CertificateSha256, BinaryPath).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/faarules/faarules.go`:
- Around line 157-165: Add an inline comment explaining that when a ProcessEntry
has PlatformBinary == true the process is identified solely by SigningID
(prefixed with "platform:") and other identifier fields (CDHash,
CertificateSha256, BinaryPath) are intentionally ignored; place this comment
immediately before or after the existing continue in the loop that iterates
item.Processes so it's clear why the code returns early in that branch
(referencing PlatformBinary, SigningID, CDHash, CertificateSha256, BinaryPath).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a948079f-e182-4af2-b405-e69920b12f11

📥 Commits

Reviewing files that changed from the base of the PR and between 20bd606 and 39294a8.

📒 Files selected for processing (1)
  • internal/faarules/faarules.go

@russellhancox russellhancox marked this pull request as ready for review March 25, 2026 21:49
@russellhancox russellhancox requested a review from a team as a code owner March 25, 2026 21:49
@russellhancox russellhancox merged commit f58b72c into main Mar 27, 2026
3 checks passed
@russellhancox russellhancox deleted the rah/snt-200-add-faa-policy-import-to-santa-rule-importer branch March 27, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants