Add current-user AI storage permission audits (#773)

This commit is contained in:
SirBroccoli authored and GitHub committed 2026-10-11 16:02:56 +02:00
1 parent 95505dc15b
commit fd830f0c26
7 files changed
+300

No files matched your search

@@ -0,0 +1,59 @@
# Title: Interesting Files - AI Storage Permissions
# ID: IF_AI_storage_permissions
# Author: PEASS-ng contributors
# Last Update: 11-10-2026
# Description: Review current-user AI storage directory permissions without reading stored data
# License: GNU GPL
# Version: 1.0
# Mitre: T1083
# Functions Used: print_2title
# Global Variables: $HOME, $SEARCH_IN_FOLDER
# Initial Functions:
# Generated Global Variables: $ai_mode, $ai_shared, $ai_relative, $ai_link, $ai_tool, $ai_found, $ai_path, $ai_ancestor
# Fat linpeas: 0
# Small linpeas: 0
if [ -z "$SEARCH_IN_FOLDER" ] && [ -n "$HOME" ]; then
print_2title "Current-user AI storage directory permissions" "T1083"
(
case "$HOME" in /*) ;; *) exit ;; esac
printf '%s\n' "Directory modes only; review ACLs separately. Stored data may include private conversations and configuration."
ai_found=0
while IFS='|' read -r ai_tool ai_relative; do
ai_path="$HOME/$ai_relative"
# Check every ancestor before inspecting the directory. Do not follow links.
ai_ancestor="$ai_path"
ai_link=0
while [ "$ai_ancestor" != / ] && [ -n "$ai_ancestor" ]; do
if [ -L "$ai_ancestor" ]; then ai_link=1; break; fi
ai_ancestor=${ai_ancestor%/*}
done
if [ "$ai_link" = 1 ]; then
printf ' %s: skipped symbolic link in directory path\n' "$ai_tool"
continue
fi
[ -d "$ai_path" ] || continue
ai_found=1
ai_mode=$(LC_ALL=C ls -ld "$ai_path" 2>/dev/null | awk '{print $1}')
if [ -z "$ai_mode" ]; then
printf ' %s: directory permissions unavailable\n' "$ai_tool"
continue
fi
printf ' %s: %s (%s)\n' "$ai_tool" "$ai_path" "$ai_mode"
ai_shared=$(printf '%s' "$ai_mode" | cut -c5-10)
case "$ai_shared" in
*[rwxst]*) printf ' Review group/other access to potentially sensitive AI storage.\n' ;;
esac
done <<'AI_STORAGE_ROOTS'
Codex|.codex
Claude Code|.claude
Cursor CLI|.cursor
Cursor desktop (Linux)|.config/Cursor
Cursor desktop (macOS)|Library/Application Support/Cursor
Antigravity CLI|.gemini/antigravity-cli
Grok|.grok
AI_STORAGE_ROOTS
[ "$ai_found" = 1 ] || printf '%s\n' " No supported storage directories found."
)
echo ""
fi
@@ -0,0 +1,64 @@
import os
import shlex
import subprocess
import tempfile
import unittest
from pathlib import Path
class AIStoragePermissionsTests(unittest.TestCase):
module = (Path(__file__).resolve().parents[1] / "builder" / "linpeas_parts"
/ "9_interesting_files" / "30_AI_storage_permissions.sh")
def run_audit(self, home, search_in_folder=""):
script = ("print_2title() { printf '%s\\n' \"$1\"; }\n"
+ ". " + shlex.quote(str(self.module)))
env = dict(os.environ, HOME=str(home), SEARCH_IN_FOLDER=search_in_folder)
return subprocess.run(["sh", "-c", script], env=env, capture_output=True,
text=True, timeout=5, check=True).stdout
def test_private_and_shared_directories_without_content_disclosure(self):
with tempfile.TemporaryDirectory() as tmp:
home = Path(tmp).resolve() / "home with spaces"
private = home / ".codex"
shared = home / ".grok"
private.mkdir(parents=True)
shared.mkdir()
private.chmod(0o700)
shared.chmod(0o770)
(shared / "private-data.txt").write_text("SECRET_FIXTURE_DO_NOT_PRINT")
# Opening this pipe would block: the audit must never enumerate/read bodies.
os.mkfifo(shared / "stored-data-pipe")
output = self.run_audit(home)
self.assertIn(str(private), output)
self.assertIn(str(shared), output)
self.assertEqual(output.count("Review group/other access"), 1)
self.assertNotIn("SECRET_FIXTURE_DO_NOT_PRINT", output)
self.assertNotIn("private-data.txt", output)
self.assertNotIn("stored-data-pipe", output)
def test_symlinked_root_and_ancestor_are_skipped(self):
with tempfile.TemporaryDirectory() as tmp:
home = Path(tmp).resolve() / "home"
outside = Path(tmp).resolve() / "outside"
home.mkdir()
(outside / "antigravity-cli").mkdir(parents=True)
(home / ".codex").symlink_to(outside, target_is_directory=True)
(home / ".gemini").symlink_to(outside, target_is_directory=True)
output = self.run_audit(home)
self.assertEqual(output.count("skipped symbolic link"), 2)
self.assertNotIn(str(outside), output)
def test_folder_scoped_scan_does_not_inspect_home(self):
with tempfile.TemporaryDirectory() as tmp:
home = Path(tmp).resolve()
(home / ".claude").mkdir()
self.assertEqual(self.run_audit(home, str(home)), "")
def test_missing_roots_are_reported(self):
with tempfile.TemporaryDirectory() as tmp:
self.assertIn("No supported storage directories found", self.run_audit(Path(tmp).resolve()))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,79 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
using winPEAS.Info.FilesInfo;
namespace winPEAS.Tests
{
[TestClass]
public class AIStorageAuditTests
{
[TestMethod]
public void ReparseAncestorStopsInspectionBeforeRedirectedChildren()
{
string profile = @"C:\Users\example";
string path = Path.Combine(profile, ".gemini", "antigravity-cli");
bool checkedChild = false;
bool result = AIStorageAudit.HasReparsePoint(path, candidate =>
{
if (candidate == path || candidate == Path.Combine(profile, ".gemini"))
checkedChild = true;
return candidate == profile ? FileAttributes.ReparsePoint : FileAttributes.Directory;
});
Assert.IsTrue(result);
Assert.IsFalse(checkedChild);
}
[TestMethod]
public void BroadReadAndWriteAllowsRequireReview()
{
var security = new DirectorySecurity();
security.AddAccessRule(new FileSystemAccessRule(
new SecurityIdentifier(WellKnownSidType.WorldSid, null),
FileSystemRights.ReadData, AccessControlType.Allow));
security.AddAccessRule(new FileSystemAccessRule(
new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null),
FileSystemRights.WriteData, AccessControlType.Allow));
Assert.AreEqual(2, AIStorageAudit.GetBroadAccessRules(security).Count);
}
[TestMethod]
public void DeniesAdministratorsAndInheritOnlyRulesAreNotBroadRootAllows()
{
var security = new DirectorySecurity();
security.AddAccessRule(new FileSystemAccessRule(
new SecurityIdentifier(WellKnownSidType.WorldSid, null),
FileSystemRights.ReadData, AccessControlType.Deny));
security.AddAccessRule(new FileSystemAccessRule(
new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null),
FileSystemRights.FullControl, AccessControlType.Allow));
security.AddAccessRule(new FileSystemAccessRule(
new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null),
FileSystemRights.ReadData, InheritanceFlags.ContainerInherit,
PropagationFlags.InheritOnly, AccessControlType.Allow));
Assert.AreEqual(0, AIStorageAudit.GetBroadAccessRules(security).Count);
}
[TestMethod]
public void DirectoryCheckDoesNotReadStoredFiles()
{
string path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(path);
try
{
using (var file = new FileStream(Path.Combine(path, "private-data"),
FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None))
{
Assert.IsFalse(AIStorageAudit.HasReparsePoint(path));
AIStorageAudit.GetBroadAccessRules(Directory.GetAccessControl(path));
}
}
finally
{
Directory.Delete(path, true);
}
}
}
}
@@ -106,6 +106,7 @@
<Compile Include="NsclientConfigCatalogTests.cs" />
<Compile Include="SqlSetupConfigurationIndicatorTests.cs" />
<Compile Include="ArgumentParsingTests.cs" />
<Compile Include="AIStorageAuditTests.cs" />
<Compile Include="AutoLogonTests.cs" />
<Compile Include="FixedBuildIndicatorTests.cs" />
<Compile Include="HMailAndInstalledAppsTests.cs" />
@@ -127,6 +127,7 @@ namespace winPEAS.Checks
SuperPutty.PrintInfo,
PrintOffice365EndpointsSyncedByOneDrive,
PrintCloudCreds,
PrintAIStoragePermissions,
PrintUnattendFiles,
PrintSAMBackups,
PrintNtdsZipBackups,
@@ -156,6 +157,38 @@ namespace winPEAS.Checks
}.ForEach(action => CheckRunner.Run(action, isDebug));
}
void PrintAIStoragePermissions()
{
Beaprint.MainPrint("Current-user AI storage directory permissions", "T1083");
Beaprint.InfoPrint("Stored data may include private conversations and configuration. Broad allow ACEs require review; deny ACEs and effective access are not evaluated.");
bool found = false;
foreach (var root in AIStorageAudit.GetCurrentUserRoots())
{
try
{
if (!Directory.Exists(root.Value))
continue;
if (AIStorageAudit.HasReparsePoint(root.Value))
{
Beaprint.InfoPrint(root.Key + ": skipped reparse point in directory path");
continue;
}
found = true;
Beaprint.InfoPrint(string.Format("{0}: {1}", root.Key, root.Value));
var rules = AIStorageAudit.GetBroadAccessRules(Directory.GetAccessControl(root.Value));
foreach (string rule in rules)
Beaprint.BadPrint(" Review broad directory access: " + rule);
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException
|| ex is System.Security.SecurityException)
{
Beaprint.InfoPrint(root.Key + ": directory permissions unavailable");
}
}
if (!found)
Beaprint.NotFoundPrint();
}
void PrintCloudCreds()
{
try
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
namespace winPEAS.Info.FilesInfo
{
internal static class AIStorageAudit
{
internal static Dictionary<string, string> GetCurrentUserRoots()
{
var roots = new Dictionary<string, string>();
string profile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (!string.IsNullOrEmpty(profile))
{
roots.Add("Codex", Path.Combine(profile, ".codex"));
roots.Add("Claude Code", Path.Combine(profile, ".claude"));
roots.Add("Cursor CLI", Path.Combine(profile, ".cursor"));
roots.Add("Antigravity CLI", Path.Combine(profile, ".gemini", "antigravity-cli"));
roots.Add("Grok", Path.Combine(profile, ".grok"));
}
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
if (!string.IsNullOrEmpty(appData))
roots.Add("Cursor desktop", Path.Combine(appData, "Cursor"));
return roots;
}
internal static bool HasReparsePoint(string path, Func<string, FileAttributes> readAttributes = null)
{
readAttributes = readAttributes ?? File.GetAttributes;
// Inspect ancestors first so a junction cannot redirect the root check.
string parent = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(parent) && HasReparsePoint(parent, readAttributes))
return true;
return (readAttributes(path) & FileAttributes.ReparsePoint) != 0;
}
internal static List<string> GetBroadAccessRules(DirectorySecurity security)
{
var findings = new List<string>();
FileSystemRights relevant = FileSystemRights.ReadData | FileSystemRights.WriteData
| FileSystemRights.AppendData | FileSystemRights.Delete
| FileSystemRights.DeleteSubdirectoriesAndFiles | FileSystemRights.ChangePermissions
| FileSystemRights.TakeOwnership;
foreach (FileSystemAccessRule rule in security.GetAccessRules(true, true, typeof(SecurityIdentifier)))
{
if (rule.AccessControlType != AccessControlType.Allow
|| (rule.PropagationFlags & PropagationFlags.InheritOnly) != 0
|| (rule.FileSystemRights & relevant) == 0)
continue;
var sid = (SecurityIdentifier)rule.IdentityReference;
if (sid.IsWellKnown(WellKnownSidType.WorldSid)
|| sid.IsWellKnown(WellKnownSidType.AuthenticatedUserSid)
|| sid.IsWellKnown(WellKnownSidType.BuiltinUsersSid)
|| sid.IsWellKnown(WellKnownSidType.BuiltinGuestsSid))
findings.Add(string.Format("{0}: {1}", sid.Value, rule.FileSystemRights));
}
// Allow ACEs are review candidates, not an effective-access calculation.
return findings;
}
}
}
@@ -1191,6 +1191,7 @@
<Compile Include="Checks\CloudInfo.cs" />
<Compile Include="Checks\FileAnalysis.cs" />
<Compile Include="Checks\FilesInfo.cs" />
<Compile Include="Info\FilesInfo\AIStorageAudit.cs" />
<Compile Include="Checks\Globals.cs" />
<Compile Include="Checks\ISystemCheck.cs" />
<Compile Include="Checks\EventsInfo.cs" />