Skip to content

feat(armotypes): add ProfileDataRequired contract for profile-data projection#642

Merged
YakirOren merged 6 commits intomainfrom
naut-1300-profile-data-required
Apr 28, 2026
Merged

feat(armotypes): add ProfileDataRequired contract for profile-data projection#642
YakirOren merged 6 commits intomainfrom
naut-1300-profile-data-required

Conversation

@slashben
Copy link
Copy Markdown
Member

@slashben slashben commented Apr 28, 2026

Summary

  • Adds ProfileDataPattern, ProfileDataField (tagged-union), and ProfileDataRequired types to armotypes/runtimerule.go — the schema contract for NAUT-1295 rule-aware profile projection.
  • Wires ProfileDataRequired *ProfileDataRequired (omitempty) into RuntimeRule alongside ProfileDependency.
  • ProfileDataField supports custom YAML/JSON/BSON codecs: marshals as the string "all" or as a list of pattern objects.
  • ProfileDataRequired.Validate() and IsEmpty() are the single source of truth used by the rulelibrary lint (Phase 2) and node-agent load-time check.

Test plan

  • TestProfileDataPattern_YAMLRoundTrip — all four match types
  • TestProfileDataPattern_JSONRoundTrip
  • TestProfileDataField_UnmarshalYAML_All/Patterns/RejectsUnknown
  • TestProfileDataField_MarshalYAML_All/Patterns
  • TestProfileDataField_JSONRoundTrip
  • TestProfileDataField_BSONRoundTrip
  • TestProfileDataRequired_Validate_Valid/FieldEmpty/PatternMultiKey/PatternEmpty/AllAndPatterns
  • TestProfileDataRequired_IsEmpty (including nil-receiver)
  • TestRuntimeRule_ProfileDataRequired_YAML — round-trip through RuntimeRule
  • TestRuntimeRule_ProfileDataRequired_OmittedWhenNil

Related: NAUT-1300 / NAUT-1295

Summary by CodeRabbit

  • New Features
    • Added profile data requirement configuration to runtime rules, enabling per-surface declarations of needed profile data through flexible matching options.

Adds the profileDataRequired field (omitempty) alongside profileDependency
in RuntimeRule, and two integration-level tests that verify YAML round-trip
and omission when nil.
Signed-off-by: Ben <ben@armosec.io>
Copilot AI review requested due to automatic review settings April 28, 2026 13:24
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 992419ce-0da0-472a-8367-7d7f10313630

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The changes add a new profileDataRequired field to RuntimeRule with supporting types (ProfileDataRequired, ProfileDataField, ProfileDataPattern) that implement tagged-union serialization across YAML, JSON, and BSON formats, along with validation logic and comprehensive test coverage.

Changes

Cohort / File(s) Summary
Core Profile Data Implementation
armotypes/runtimerule.go
Adds ProfileDataRequired field to RuntimeRule and defines three new types: ProfileDataRequired (with optional per-surface fields and IsEmpty()/Validate() methods), ProfileDataField (tagged-union supporting either "all" string or pattern list), and ProfileDataPattern (supporting exact, prefix, suffix, or contains matching). Implements custom serialization for YAML, JSON, and BSON with validation ensuring each surface declares either "all" or at least one pattern, and each pattern sets exactly one match type.
Profile Data Test Suite
armotypes/runtimerule_profile_data_test.go
Comprehensive test coverage for profile data structures including YAML/JSON/BSON round-tripping, validation rule enforcement (valid configurations, multiple invalid states, empty patterns, conflicting "all" and patterns), serialization key verification, and integration tests confirming profileDataRequired omission from outputs when nil.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

The implementation introduces a tagged-union serialization pattern across multiple formats with multi-layer validation logic, requiring careful verification of serialization correctness and edge case handling. The test suite comprehensiveness ensures validation coverage but demands cross-format verification.

Poem

🐰 A rabbit's profile, now tracked with care,
All patterns matched or data to spare,
YAML, JSON, BSON unite,
Validation rules burning bright,
Runtime rules with details rare! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding the ProfileDataRequired contract to armotypes for profile-data projection, which aligns with the changeset's primary contribution.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 naut-1300-profile-data-required

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
armotypes/runtimerule.go (1)

134-139: Guard marshal paths against invalid union state to avoid silent data loss.

If All == true and Patterns is non-empty, marshal currently emits "all" and drops patterns. If both are unset, it emits an empty list. Consider enforcing invariants directly in marshal methods.

Suggested refactor
+func (f ProfileDataField) validateState() error {
+	if f.All && len(f.Patterns) > 0 {
+		return fmt.Errorf("profileDataField: cannot have both 'all' and pattern list")
+	}
+	if !f.All && len(f.Patterns) == 0 {
+		return fmt.Errorf("profileDataField: must declare 'all' or at least one pattern")
+	}
+	return nil
+}
+
 func (f ProfileDataField) MarshalYAML() (any, error) {
+	if err := f.validateState(); err != nil {
+		return nil, err
+	}
 	if f.All {
 		return profileDataFieldAllSentinel, nil
 	}
 	return f.Patterns, nil
 }
@@
 func (f ProfileDataField) MarshalJSON() ([]byte, error) {
+	if err := f.validateState(); err != nil {
+		return nil, err
+	}
 	if f.All {
 		return json.Marshal(profileDataFieldAllSentinel)
 	}
 	return json.Marshal(f.Patterns)
 }
@@
 func (f ProfileDataField) MarshalBSONValue() (bsontype.Type, []byte, error) {
+	if err := f.validateState(); err != nil {
+		return 0, nil, err
+	}
 	if f.All {
 		return bson.MarshalValue(profileDataFieldAllSentinel)
 	}
 	return bson.MarshalValue(f.Patterns)
 }

Also applies to: 167-172, 193-198

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

In `@armotypes/runtimerule.go` around lines 134 - 139,
ProfileDataField.MarshalYAML currently drops patterns when All is true or
silently emits an empty list when both fields are unset; change it to validate
the union invariants: in ProfileDataField.MarshalYAML, if f.All &&
len(f.Patterns) > 0 return a non-nil error describing the conflicting state
instead of returning profileDataFieldAllSentinel, and if !f.All &&
len(f.Patterns) == 0 return a non-nil error indicating the unset/empty union
rather than silently emitting an empty value; apply the same validation pattern
to the other marshal methods that use the same All vs Patterns union semantics
so conflicts and empty-unset states fail fast.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@armotypes/runtimerule_profile_data_test.go`:
- Around line 92-103: Add a negative JSON test that ensures unmarshaling "null"
into a ProfileDataField is rejected: create a test (e.g., add a case in
TestProfileDataField_JSONRoundTrip or a new TestProfileDataField_JSONNullReject)
that does json.Unmarshal([]byte("null"), &got) where got is a ProfileDataField
and assert that an error is returned (use require.Error/require.NotNil on the
error) to lock the schema contract for ProfileDataField.

In `@armotypes/runtimerule.go`:
- Around line 174-190: ProfileDataField.UnmarshalJSON currently allows JSON null
because json.Unmarshal into []ProfileDataPattern returns nil instead of error;
modify UnmarshalJSON (ProfileDataField.UnmarshalJSON) to explicitly reject JSON
null before decoding the slice (e.g., check if data equals "null" or unmarshal
into a raw json.RawMessage and detect null) and return an error instead of
treating nil patterns as a valid list, preserving existing handling for the
"all" sentinel and non-null slices by setting f.All=false and f.Patterns
accordingly.

---

Nitpick comments:
In `@armotypes/runtimerule.go`:
- Around line 134-139: ProfileDataField.MarshalYAML currently drops patterns
when All is true or silently emits an empty list when both fields are unset;
change it to validate the union invariants: in ProfileDataField.MarshalYAML, if
f.All && len(f.Patterns) > 0 return a non-nil error describing the conflicting
state instead of returning profileDataFieldAllSentinel, and if !f.All &&
len(f.Patterns) == 0 return a non-nil error indicating the unset/empty union
rather than silently emitting an empty value; apply the same validation pattern
to the other marshal methods that use the same All vs Patterns union semantics
so conflicts and empty-unset states fail fast.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6c089bbd-0e06-43b0-8d9e-daeded393f54

📥 Commits

Reviewing files that changed from the base of the PR and between ae803f2 and c7eaa04.

📒 Files selected for processing (2)
  • armotypes/runtimerule.go
  • armotypes/runtimerule_profile_data_test.go

Comment on lines +92 to +103
func TestProfileDataField_JSONRoundTrip(t *testing.T) {
for _, f := range []ProfileDataField{
{All: true},
{Patterns: []ProfileDataPattern{{Prefix: "/x"}, {Suffix: "/y"}}},
} {
b, err := json.Marshal(f)
require.NoError(t, err)
var got ProfileDataField
require.NoError(t, json.Unmarshal(b, &got))
assert.Equal(t, f, got)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a JSON negative test for null input on ProfileDataField.

Current JSON coverage checks valid forms only. Please add a rejection test for null to lock the schema contract.

Suggested test addition
+func TestProfileDataField_UnmarshalJSON_RejectsNull(t *testing.T) {
+	var f ProfileDataField
+	err := json.Unmarshal([]byte(`null`), &f)
+	assert.Error(t, err)
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestProfileDataField_JSONRoundTrip(t *testing.T) {
for _, f := range []ProfileDataField{
{All: true},
{Patterns: []ProfileDataPattern{{Prefix: "/x"}, {Suffix: "/y"}}},
} {
b, err := json.Marshal(f)
require.NoError(t, err)
var got ProfileDataField
require.NoError(t, json.Unmarshal(b, &got))
assert.Equal(t, f, got)
}
}
func TestProfileDataField_JSONRoundTrip(t *testing.T) {
for _, f := range []ProfileDataField{
{All: true},
{Patterns: []ProfileDataPattern{{Prefix: "/x"}, {Suffix: "/y"}}},
} {
b, err := json.Marshal(f)
require.NoError(t, err)
var got ProfileDataField
require.NoError(t, json.Unmarshal(b, &got))
assert.Equal(t, f, got)
}
}
func TestProfileDataField_UnmarshalJSON_RejectsNull(t *testing.T) {
var f ProfileDataField
err := json.Unmarshal([]byte(`null`), &f)
assert.Error(t, err)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@armotypes/runtimerule_profile_data_test.go` around lines 92 - 103, Add a
negative JSON test that ensures unmarshaling "null" into a ProfileDataField is
rejected: create a test (e.g., add a case in TestProfileDataField_JSONRoundTrip
or a new TestProfileDataField_JSONNullReject) that does
json.Unmarshal([]byte("null"), &got) where got is a ProfileDataField and assert
that an error is returned (use require.Error/require.NotNil on the error) to
lock the schema contract for ProfileDataField.

Comment thread armotypes/runtimerule.go
Comment on lines +174 to +190
func (f *ProfileDataField) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err == nil {
if s != profileDataFieldAllSentinel {
return fmt.Errorf("profileDataField: scalar must be %q, got %q", profileDataFieldAllSentinel, s)
}
f.All = true
f.Patterns = nil
return nil
}
var patterns []ProfileDataPattern
if err := json.Unmarshal(data, &patterns); err != nil {
return fmt.Errorf("profileDataField: must be string %q or list: %w", profileDataFieldAllSentinel, err)
}
f.All = false
f.Patterns = patterns
return nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject JSON null in ProfileDataField.UnmarshalJSON.

On Line 185, json.Unmarshal into []ProfileDataPattern accepts null, so profileDataField can decode from null even though the contract allows only "all" or a list.

Suggested fix
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
@@
 func (f *ProfileDataField) UnmarshalJSON(data []byte) error {
+	if bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
+		return fmt.Errorf("profileDataField: must be string %q or list, got null", profileDataFieldAllSentinel)
+	}
+
 	var s string
 	if err := json.Unmarshal(data, &s); err == nil {
 		if s != profileDataFieldAllSentinel {
 			return fmt.Errorf("profileDataField: scalar must be %q, got %q", profileDataFieldAllSentinel, s)
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (f *ProfileDataField) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err == nil {
if s != profileDataFieldAllSentinel {
return fmt.Errorf("profileDataField: scalar must be %q, got %q", profileDataFieldAllSentinel, s)
}
f.All = true
f.Patterns = nil
return nil
}
var patterns []ProfileDataPattern
if err := json.Unmarshal(data, &patterns); err != nil {
return fmt.Errorf("profileDataField: must be string %q or list: %w", profileDataFieldAllSentinel, err)
}
f.All = false
f.Patterns = patterns
return nil
import (
"bytes"
"encoding/json"
"fmt"
)
func (f *ProfileDataField) UnmarshalJSON(data []byte) error {
if bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
return fmt.Errorf("profileDataField: must be string %q or list, got null", profileDataFieldAllSentinel)
}
var s string
if err := json.Unmarshal(data, &s); err == nil {
if s != profileDataFieldAllSentinel {
return fmt.Errorf("profileDataField: scalar must be %q, got %q", profileDataFieldAllSentinel, s)
}
f.All = true
f.Patterns = nil
return nil
}
var patterns []ProfileDataPattern
if err := json.Unmarshal(data, &patterns); err != nil {
return fmt.Errorf("profileDataField: must be string %q or list: %w", profileDataFieldAllSentinel, err)
}
f.All = false
f.Patterns = patterns
return nil
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@armotypes/runtimerule.go` around lines 174 - 190,
ProfileDataField.UnmarshalJSON currently allows JSON null because json.Unmarshal
into []ProfileDataPattern returns nil instead of error; modify UnmarshalJSON
(ProfileDataField.UnmarshalJSON) to explicitly reject JSON null before decoding
the slice (e.g., check if data equals "null" or unmarshal into a raw
json.RawMessage and detect null) and return an error instead of treating nil
patterns as a valid list, preserving existing handling for the "all" sentinel
and non-null slices by setting f.All=false and f.Patterns accordingly.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new schema contract in armotypes to describe which subsets of profile data a runtime rule requires (for rule-aware profile-data projection), including custom YAML/JSON/BSON encoding and validation, plus comprehensive round-trip tests.

Changes:

  • Introduces ProfileDataPattern, ProfileDataField (tagged-union), and ProfileDataRequired with validation helpers.
  • Adds ProfileDataRequired *ProfileDataRequired (omitempty) to RuntimeRule.
  • Adds YAML/JSON/BSON codec + round-trip/validation tests for the new contract.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
armotypes/runtimerule.go Adds ProfileDataRequired to RuntimeRule and implements the new profile-data requirement types with custom codecs + validation.
armotypes/runtimerule_profile_data_test.go Adds round-trip tests for YAML/JSON/BSON and validation/emptiness behavior for the new types.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread armotypes/runtimerule.go
Comment on lines +259 to +273
for name, field := range map[string]*ProfileDataField{
"opens": p.Opens,
"execs": p.Execs,
"capabilities": p.Capabilities,
"syscalls": p.Syscalls,
"endpoints": p.Endpoints,
"egressDomains": p.EgressDomains,
"egressAddresses": p.EgressAddresses,
"ingressDomains": p.IngressDomains,
"ingressAddresses": p.IngressAddresses,
} {
if field == nil {
continue
}
if err := validateProfileDataField(name, field); err != nil {
Comment thread armotypes/runtimerule.go
var patterns []ProfileDataPattern
if err := json.Unmarshal(data, &patterns); err != nil {
return fmt.Errorf("profileDataField: must be string %q or list: %w", profileDataFieldAllSentinel, err)
}
Comment thread armotypes/runtimerule.go
Comment on lines +167 to +172
func (f ProfileDataField) MarshalJSON() ([]byte, error) {
if f.All {
return json.Marshal(profileDataFieldAllSentinel)
}
return json.Marshal(f.Patterns)
}
assert.Equal(t, f, got)
}
}

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Coverage Report for CI Build 25055508324

Warning

No base build found for commit ae803f2 on main.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 43.169%

Details

  • Patch coverage: 20 uncovered changes across 1 file (103 of 123 lines covered, 83.74%).

Uncovered Changes

File Changed Covered %
armotypes/runtimerule.go 123 103 83.74%

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 4626
Covered Lines: 1997
Line Coverage: 43.17%
Coverage Strength: 5.87 hits per line

💛 - Coveralls

JSON null was silently accepted via the []ProfileDataPattern path, producing
an invalid union state (All=false, Patterns=nil). Now returns an error, and
a test locks the contract.
Signed-off-by: Ben <ben@armosec.io>
@YakirOren YakirOren merged commit 111099f into main Apr 28, 2026
3 checks passed
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.

3 participants