Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ The expanded changed-file footprint now uses 7,240 baseline lines and 479 baseli

The current slice uses 8,665 lines and 530 control-flow lines. The complete reduction gate remains open.

## Closed coverage-source slice

This slice replaces the coverage-kind, reason, and scope tuple with one closed internal source. Trace fields now derive from that source once.

Validated actor evidence is bound to its originating candidate snapshot. The evaluation no longer revalidates the same actor batch after the boundary accepts it.

The slice removes another 100 production lines and nine control-flow lines. It also removes 28 redundant state-test lines.

The cumulative footprint uses 8,565 lines and 521 control-flow lines. The complete reduction gate remains open.

## Derived path-metadata slice

This slice removes path-domain tags already expressed by the closed domain type. It also removes base tags already expressed by separate real, intent, and fallback views.

The slice removes another 45 production lines and eight test lines. It does not change the control-flow count.

The cumulative footprint uses 8,520 lines and 521 control-flow lines. The complete reduction gate remains open.

## Preliminary coverage and risk

The audit used `dotnet-coverage` 18.10.0 and `crap4dotnet` 0.1.1.
Expand Down
14 changes: 4 additions & 10 deletions src/Netclaw.Actors.Tests/Tools/DispatchingToolExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1546,14 +1546,11 @@ public void Shell_policy_trace_caps_rows_without_authority_change()
for (var index = 0; index < 300; index++)
{
builder.AddCoverage(
ShellPolicyTraceStage.ReviewedSafePolicy,
ShellPolicyCoverageSource.ReviewedSafeReal,
new ShellPolicyCandidate(
new ShellPolicyCandidateId(index),
BashCandidate($"/usr/bin/tool-{index}"),
SourceOccurrence: null),
ShellCoverageKind.ReviewedSafePolicy,
ShellPolicyReason.ReviewedSafePhrase,
ShellScopeRelation.UnderRealRoot);
SourceOccurrence: null));
}

var decision = ToolAuthorizationDecision.Allow(ToolAllowReason.SafeVerbInTrustedScope);
Expand Down Expand Up @@ -1605,14 +1602,11 @@ public void Shell_policy_trace_logs_one_row_for_malicious_executable_text()
var executor = CreateApprovalGatedShellExecutor(logger: logger);
var builder = new ShellPolicyDecisionTraceBuilder();
builder.AddCoverage(
ShellPolicyTraceStage.ReviewedSafePolicy,
ShellPolicyCoverageSource.ReviewedSafeReal,
new ShellPolicyCandidate(
new ShellPolicyCandidateId(0),
BashCandidate($"/usr/bin/{secret}\r\n\u202Espoof"),
SourceOccurrence: null),
ShellCoverageKind.ReviewedSafePolicy,
ShellPolicyReason.ReviewedSafePhrase,
ShellScopeRelation.UnderRealRoot);
SourceOccurrence: null));
var trace = builder.Complete(
ToolAuthorizationDecision.Allow(ToolAllowReason.SafeVerbInTrustedScope));

Expand Down
188 changes: 79 additions & 109 deletions src/Netclaw.Actors.Tests/Tools/ShellPolicyEvaluationTests.cs

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions src/Netclaw.Actors/Tools/ScopedShellSafeVerbPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,8 @@ private static bool AllAuthoredPathsStayWithinRoots(
&& pathStyle != ShellPathStyle.Posix
|| fact.Source.AuthoredPathShape == ShellPathShape.Windows
&& pathStyle != ShellPathStyle.Windows
|| fact.Source.DomainKind is not
(ShellPolicyPathDomainKind.Exact or ShellPolicyPathDomainKind.FiniteSet)
|| fact.Source.Domain is not
(ShellValueDomain.Exact or ShellValueDomain.FiniteSet)
|| fact.State != ShellPolicyPathResolutionState.Known
|| fact.Paths.Count == 0
|| fact.Paths.Any(path =>
Expand All @@ -340,8 +340,8 @@ private static bool AllEffectivePathsStayWithinIntent(
foreach (var fact in resolvedPaths.Facts.Where(static fact =>
fact.Source.Origin == ShellPolicyPathOrigin.EffectiveArgument))
{
if (fact.Source.DomainKind is not
(ShellPolicyPathDomainKind.Exact or ShellPolicyPathDomainKind.FiniteSet)
if (fact.Source.Domain is not
(ShellValueDomain.Exact or ShellValueDomain.FiniteSet)
|| fact.State != ShellPolicyPathResolutionState.Known
|| fact.Paths.Count == 0
|| fact.Paths.Any(path => !IsSafePath(
Expand All @@ -357,7 +357,7 @@ private static bool AllEffectivePathsStayWithinIntent(
fact.Source.Origin == ShellPolicyPathOrigin.Redirect))
{
if (fact.Source.RedirectMode != FileRedirectMode.Input
|| fact.Source.DomainKind != ShellPolicyPathDomainKind.Exact
|| fact.Source.Domain is not ShellValueDomain.Exact
|| fact.State != ShellPolicyPathResolutionState.Known
|| fact.Paths.Count != 1
|| !IsSafePath(
Expand Down
5 changes: 5 additions & 0 deletions src/Netclaw.Actors/Tools/ShellApprovalEvidence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,20 @@ internal sealed class ValidatedShellGrantEvidence

private ValidatedShellGrantEvidence(
PersistentGrantStoreStatus persistentStore,
IReadOnlyList<ShellPolicyCandidate> sourceCandidates,
ValidatedShellGrantCandidateEvidence[] candidateEvidence,
ToolApprovalMatch[] approvalMatches)
{
PersistentStore = persistentStore;
SourceCandidates = sourceCandidates;
_candidateEvidence = Array.AsReadOnly(candidateEvidence);
_approvalMatches = Array.AsReadOnly(approvalMatches);
}

internal PersistentGrantStoreStatus PersistentStore { get; }

internal IReadOnlyList<ShellPolicyCandidate> SourceCandidates { get; }

internal IReadOnlyList<ValidatedShellGrantCandidateEvidence> CandidateEvidence => _candidateEvidence;

internal IReadOnlyList<ToolApprovalMatch> ApprovalMatches => _approvalMatches;
Expand Down Expand Up @@ -207,6 +211,7 @@ internal static bool TryCreate(

evidence = new ValidatedShellGrantEvidence(
result.PersistentStore,
candidates,
validated,
approvalMatches.ToArray());
return true;
Expand Down
16 changes: 4 additions & 12 deletions src/Netclaw.Actors/Tools/ShellPolicyCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -378,9 +378,7 @@ internal static ShellPolicyStageResult ApprovalExemptSideEffects(
{
var result = evaluation.Cover(
candidate,
ShellCoverageKind.ReviewedSafePolicy,
ShellPolicyReason.ApprovalExemptSideEffect,
ShellScopeRelation.None);
ShellPolicyCoverageSource.ApprovalExemptSideEffect);
if (result is not ShellPolicyStageResult.Continue)
return result;
}
Expand All @@ -407,9 +405,7 @@ internal static ShellPolicyStageResult ExactOneTime(
{
var result = evaluation.Cover(
candidate,
ShellCoverageKind.OneTime,
ShellPolicyReason.OneTimeGrant,
ShellScopeRelation.None);
ShellPolicyCoverageSource.OneTime);
if (result is not ShellPolicyStageResult.Continue)
return result;
}
Expand Down Expand Up @@ -460,9 +456,7 @@ internal static ShellPolicyStageResult RealScope(

var result = evaluation.Cover(
candidate,
ShellCoverageKind.ReviewedSafePolicy,
ShellPolicyReason.ReviewedSafePhrase,
ShellScopeRelation.UnderRealRoot);
ShellPolicyCoverageSource.ReviewedSafeReal);
if (result is not ShellPolicyStageResult.Continue)
return result;
}
Expand Down Expand Up @@ -499,9 +493,7 @@ internal static ShellPolicyStageResult IntentScope(

var result = evaluation.Cover(
candidate,
ShellCoverageKind.ReviewedSafePolicy,
ShellPolicyReason.ReviewedSafePhrase,
ShellScopeRelation.UnderIntentRoot);
ShellPolicyCoverageSource.ReviewedSafeIntent);
if (result is not ShellPolicyStageResult.Continue)
return result;
}
Expand Down
63 changes: 45 additions & 18 deletions src/Netclaw.Actors/Tools/ShellPolicyDecisionTrace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,21 +133,59 @@ nearMiss is null
}

internal void AddCoverage(
ShellPolicyTraceStage stage,
ShellPolicyCoverageSource source,
ShellPolicyCandidate candidate,
ShellCoverageKind coverage,
ShellPolicyReason reason,
ShellScopeRelation scopeRelation,
DateTimeOffset? grantTimestamp = null)
=> AddDetail(new ShellPolicyTraceRow(
{
var (stage, coverage, reason, scope) = source switch
{
ShellPolicyCoverageSource.OneTime => (
ShellPolicyTraceStage.OneTimeApproval,
ShellCoverageKind.OneTime,
ShellPolicyTraceReason.OneTimeGrant,
ShellScopeRelation.None),
ShellPolicyCoverageSource.Session => (
ShellPolicyTraceStage.StoredGrantMatch,
ShellCoverageKind.Session,
ShellPolicyTraceReason.SessionGrant,
ShellScopeRelation.ThisChat),
ShellPolicyCoverageSource.PersistentGlobal => (
ShellPolicyTraceStage.StoredGrantMatch,
ShellCoverageKind.PersistentGlobal,
ShellPolicyTraceReason.PersistentGlobalGrant,
ShellScopeRelation.Global),
ShellPolicyCoverageSource.PersistentFolder => (
ShellPolicyTraceStage.StoredGrantMatch,
ShellCoverageKind.PersistentFolder,
ShellPolicyTraceReason.PersistentFolderGrant,
ShellScopeRelation.UnderGrantRoot),
ShellPolicyCoverageSource.ReviewedSafeReal => (
ShellPolicyTraceStage.ReviewedSafePolicy,
ShellCoverageKind.ReviewedSafePolicy,
ShellPolicyTraceReason.ReviewedSafePhrase,
ShellScopeRelation.UnderRealRoot),
ShellPolicyCoverageSource.ReviewedSafeIntent => (
ShellPolicyTraceStage.ReviewedSafePolicy,
ShellCoverageKind.ReviewedSafePolicy,
ShellPolicyTraceReason.ReviewedSafePhrase,
ShellScopeRelation.UnderIntentRoot),
ShellPolicyCoverageSource.ApprovalExemptSideEffect => (
ShellPolicyTraceStage.ReviewedSafePolicy,
ShellCoverageKind.ReviewedSafePolicy,
ShellPolicyTraceReason.ApprovalExemptSideEffect,
ShellScopeRelation.None),
_ => throw new ArgumentOutOfRangeException(nameof(source))
};
AddDetail(new ShellPolicyTraceRow(
stage,
ShellPolicyTraceOutcome.Covered,
ToTraceReason(reason),
reason,
candidate.Id,
GetExecutableBasename(candidate.Candidate),
coverage,
scopeRelation,
scope,
grantTimestamp));
}

internal ShellPolicyDecisionTrace Complete(ToolAuthorizationDecision decision)
{
Expand Down Expand Up @@ -286,17 +324,6 @@ private static ShellPolicyTraceRow ToCompletionRow(ToolAuthorizationDecision dec
_ => ShellPolicyTraceReason.None,
};

private static ShellPolicyTraceReason ToTraceReason(ShellPolicyReason reason) => reason switch
{
ShellPolicyReason.OneTimeGrant => ShellPolicyTraceReason.OneTimeGrant,
ShellPolicyReason.SessionGrant => ShellPolicyTraceReason.SessionGrant,
ShellPolicyReason.PersistentGlobalGrant => ShellPolicyTraceReason.PersistentGlobalGrant,
ShellPolicyReason.PersistentFolderGrant => ShellPolicyTraceReason.PersistentFolderGrant,
ShellPolicyReason.ReviewedSafePhrase => ShellPolicyTraceReason.ReviewedSafePhrase,
ShellPolicyReason.ApprovalExemptSideEffect => ShellPolicyTraceReason.ApprovalExemptSideEffect,
_ => ShellPolicyTraceReason.None,
};

private static ShellPolicyTraceReason ToTraceReason(ShellApprovalNearMissReason reason) => reason switch
{
ShellApprovalNearMissReason.OutsideDirectory => ShellPolicyTraceReason.OutsideDirectory,
Expand Down
Loading
Loading