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
5 changes: 2 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
<PackageVersion Include="SlackNet.Extensions.DependencyInjection" Version="$(SlackNetVersion)" />
<PackageVersion Include="Cronos" Version="0.13.0" />
<PackageVersion Include="Netclaw.SkillClient" Version="0.4.1" />
<PackageVersion Include="ShellSyntaxTree" Version="0.2.0" />
<PackageVersion Include="ShellSyntaxTree" Version="0.3.0-alpha" />
<PackageVersion Include="Termina" Version="0.15.1" />
</ItemGroup>
<!-- Serialization -->
Expand Down Expand Up @@ -124,7 +124,6 @@
<ItemGroup>
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.301" />
</ItemGroup>

<!-- Transitive audit suppress: Microsoft.Data.Sqlite → SQLitePCLRaw.lib.e_sqlite3 2.1.11
is flagged by NuGetAudit (GHSA-2m69-gcr7-jv3q, CVE-2025-6965 — SQLite < 3.50.2
memory corruption in aggregate-term handling). No patched version of
Expand All @@ -135,4 +134,4 @@
<ItemGroup>
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-2m69-gcr7-jv3q" />
</ItemGroup>
</Project>
</Project>
24 changes: 22 additions & 2 deletions src/Netclaw.Actors.Tests/Tools/ShellApprovalCaseCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,10 @@ public static class ShellApprovalCases
Approvals.None,
ExpectedApproval.Require(["cat"])),
Case(
"safe-verb-namespaced-external-path-prompts",
"safe-verb-bash-provider-looking-relative-path-allows",
Bash("cat filesystem::/etc/netclaw.secret"),
Approvals.None,
ExpectedApproval.Require(["cat"])),
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"safe-verb-external-redirect-prompts",
Bash($"git status > {TemporaryFile("netclaw-approval-matrix.txt")}"),
Expand Down Expand Up @@ -468,6 +468,11 @@ public static class ShellApprovalCases
Bash("bash -lc \"netclaw daemon stop\""),
Approvals.None,
ExpectedApproval.Deny("hard_deny_self_destructive")),
Case(
"hard-deny-dash-shell-blocks",
Bash("/bin/dash -c \"netclaw daemon stop\""),
Approvals.PersistentAnywhere("/bin/dash"),
ExpectedApproval.Deny("hard_deny_self_destructive")),
Case(
"nested-shell-prompts-for-inner-command",
Bash("bash -lc \"git push\""),
Expand Down Expand Up @@ -533,6 +538,21 @@ public static class ShellApprovalCases
Bash("git status 2>&1-"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"combined-output-project-redirect-safe-verb-allows",
Bash("git status &> result.log"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"combined-output-append-project-redirect-safe-verb-allows",
Bash("git status &>> result.log"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"numeric-source-project-redirect-safe-verb-allows",
Bash("git status 3> result.log"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"fd-dup-redirect-mutating-no-grant-prompts-not-messy",
Bash("git push origin dev 2>&1 | tail -2"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
| safe-verb-external-path-prompts | Personal | Project | Interactive | cat /etc/passwd | none | RequiresApproval | approval required | cat | No |
| safe-verb-quoted-external-path-prompts | Personal | Project | Interactive | cat "/etc/netclaw.secret" | none | RequiresApproval | approval required | cat | No |
| safe-verb-traversal-external-path-prompts | Personal | Project | Interactive | cat safe/../../../../../../etc/netclaw.secret | none | RequiresApproval | approval required | cat | No |
| safe-verb-namespaced-external-path-prompts | Personal | Project | Interactive | cat filesystem::/etc/netclaw.secret | none | RequiresApproval | approval required | cat | No |
| safe-verb-bash-provider-looking-relative-path-allows | Personal | Project | Interactive | cat filesystem::/etc/netclaw.secret | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| safe-verb-external-redirect-prompts | Personal | Project | Interactive | git status > {TempPath}netclaw-approval-matrix.txt | none | RequiresApproval | approval required | git status | No |
| mutating-verb-project-prompts | Personal | Project | Interactive | git push | none | RequiresApproval | approval required | git push | No |
| all-safe-compound-allows | Personal | Project | Interactive | git status && git log | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
Expand Down Expand Up @@ -56,6 +56,7 @@
| three-step-release-prompts | Personal | Project | Interactive | git add . && git commit -m fix && git push origin dev | none | RequiresApproval | approval required | git add, git commit, git push origin dev | No |
| hard-deny-pipeline-tail-blocks | Personal | Project | Interactive | echo safe \| netclaw daemon stop | none | Denied | hard_deny_self_destructive | none | Not applicable |
| hard-deny-nested-shell-blocks | Personal | Project | Interactive | bash -lc "netclaw daemon stop" | none | Denied | hard_deny_self_destructive | none | Not applicable |
| hard-deny-dash-shell-blocks | Personal | Project | Interactive | /bin/dash -c "netclaw daemon stop" | persistent[anywhere]:/bin/dash | Denied | hard_deny_self_destructive | none | Not applicable |
| nested-shell-prompts-for-inner-command | Personal | Project | Interactive | bash -lc "git push" | none | RequiresApproval | approval required | git push | No |
| nested-shell-inner-grant-allows | Personal | Project | Interactive | bash -lc "git push" | persistent[anywhere]:git push | Allowed | StoredApproval | none | Not applicable |
| nested-shell-wrapper-grant-does-not-cover-inner-command | Personal | Project | Interactive | bash -lc "git push" | persistent[anywhere]:bash | RequiresApproval | approval required | git push | No |
Expand All @@ -69,6 +70,9 @@
| fd-dup-redirect-safe-pipeline-allows | Personal | Project | Interactive | git log --oneline -5 2>&1 \| tail -20 | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| fd-close-redirect-safe-verb-allows | Personal | Project | Interactive | git status 2>&- | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| fd-move-redirect-safe-verb-allows | Personal | Project | Interactive | git status 2>&1- | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| combined-output-project-redirect-safe-verb-allows | Personal | Project | Interactive | git status &> result.log | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| combined-output-append-project-redirect-safe-verb-allows | Personal | Project | Interactive | git status &>> result.log | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| numeric-source-project-redirect-safe-verb-allows | Personal | Project | Interactive | git status 3> result.log | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| fd-dup-redirect-mutating-no-grant-prompts-not-messy | Personal | Project | Interactive | git push origin dev 2>&1 \| tail -2 | none | RequiresApproval | approval required | git push origin dev, tail | No |
| dynamic-fd-redirect-fails-closed | Personal | Project | Interactive | git status 2>&$FD | none | RequiresApproval | approval required | none | Yes |
| background-list-prompts-for-mutating-tail | Personal | Project | Interactive | git status & git push | none | RequiresApproval | approval required | none | Yes |
Expand Down
11 changes: 11 additions & 0 deletions src/Netclaw.Security.Tests/ShellApprovalMatcherMultilineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,15 @@ public void ExtractCandidates_multiline_surfaces_each_verb()
Assert.Contains(candidates, c => c.Verb == "cd" && c.Directory == "/tmp");
Assert.Contains(candidates, c => c.Verb == "rm" && c.Directory == "/tmp/foo");
}

[Fact(SkipUnless = nameof(IsPosix), Skip = "POSIX-only path semantics")]
public void ExtractCandidates_uncertain_cwd_without_absolute_scope_fails_closed()
{
// A newline runs git even if cd fails. Its cwd can therefore be the
// original directory or /tmp, and no persistent scope is safe.
var arguments = Args("cd /tmp\ngit status");

Assert.Empty(_matcher.ExtractCandidates(new ToolName("shell_execute"), arguments));
Assert.True(_matcher.IsMessy(new ToolName("shell_execute"), arguments));
}
}
43 changes: 43 additions & 0 deletions src/Netclaw.Security.Tests/ShellApprovalMatcherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,49 @@ public void ExtractCandidates_recurses_into_bash_dash_c_with_cd_attribution_inta
Assert.Contains(candidates, c => c.Verb == "git push" && c.Directory == "/repo");
}

[Fact(SkipUnless = nameof(IsPosix), Skip = "POSIX-only path semantics")]
public void ExtractCandidates_bundled_wrapper_inherits_outer_proven_cwd()
{
var candidate = Assert.Single(_matcher.ExtractCandidates(
new ToolName("shell_execute"),
Args(
"cd /tmp && bash -lc \"cat relative.txt\"",
"/work")),
candidate => candidate.Verb == "cat");

Assert.Equal("/tmp", candidate.Directory);
}

[Fact(SkipUnless = nameof(IsPosix), Skip = "POSIX-only path semantics")]
public void Redirect_to_symlink_target_fails_closed()
{
var root = Path.Combine(Path.GetTempPath(), $"netclaw-redirect-symlink-{Guid.NewGuid():N}");
var projectDirectory = Path.Combine(root, "project");
var externalDirectory = Path.Combine(root, "external");
var externalFile = Path.Combine(externalDirectory, "result.log");
var redirectTarget = Path.Combine(projectDirectory, "result.log");
Directory.CreateDirectory(projectDirectory);
Directory.CreateDirectory(externalDirectory);
File.WriteAllText(externalFile, "external");
File.CreateSymbolicLink(redirectTarget, externalFile);

try
{
var arguments = Args("git status > result.log", projectDirectory);

Assert.Empty(_matcher.ExtractCandidates(
new ToolName("shell_execute"),
arguments));
Assert.True(_matcher.IsMessy(
new ToolName("shell_execute"),
arguments));
}
finally
{
Directory.Delete(root, recursive: true);
}
}

[Fact(SkipUnless = nameof(IsPosix), Skip = "POSIX-only path semantics")]
public void ExtractCandidates_prefers_explicit_path_arg_over_cd_attribution()
{
Expand Down
Loading
Loading