diff --git a/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml b/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml index 0ff23289f2..aa64529ff4 100644 --- a/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml +++ b/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml @@ -158,6 +158,17 @@ spec: 'Computer Configuration\Policies\Windows Settings\Security Settings\Account Policies\Password Policy\Minimum password length' query: | SELECT 1 FROM security_profile_info WHERE minimum_password_length >= 14; + powershell: | + $netAccountsOutput = net accounts + + $minPwdLine = $netAccountsOutput | Where-Object {$_ -match "Minimum password length"} + + if ($minPwdLine -match "Minimum password length:\s*(\d+)") { + $minPasswordLength = [int]$matches[1] + if ($minPasswordLength -ge 14) { + Write-Output "1" + } + } purpose: Informational tags: compliance, CIS, CIS_Level1, premium contributors: marcosd4h @@ -698,6 +709,11 @@ spec: considered unprotected. Use the additional results (percent_encrypted, conversion_status, etc.) to help narrow down the specific reason why Windows considers the volume unprotected." platform: windows + powershell: | + $bitlockerInfo = Get-BitLockerVolume -MountPoint "C:" + if ($bitlockerInfo.ProtectionStatus -eq 1) { + Write-Output 1 + } tags: compliance, hardening, built-in, critical contributors: defensivedepth --- @@ -915,6 +931,19 @@ spec: description: Checks the status of antivirus and signature updates from the Windows Security Center. resolution: "Ensure Windows Defender or your third-party antivirus is running, up to date, and visible in the Windows Security Center." tags: compliance, malware, hardening, built-in + powershell: | + $avProducts = Get-CimInstance -Namespace "root/SecurityCenter2" -ClassName + AntiVirusProduct -ErrorAction SilentlyContinue + + if ($avProducts) { + $goodProducts = $avProducts | Where-Object { + # Check that the antivirus appears enabled (bit 0x10) and definitions are up‐to‐date (bit 0x100) + ($_.productState -band 0x10) -eq 0x10 -and ($_.productState -band 0x100) -eq 0x100 + } + if ($goodProducts) { + Write-Output "1" + } + } platform: windows contributors: GuillaumeRoss --- @@ -969,6 +998,86 @@ spec: query: SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM startup_items WHERE path = "regsvr32" AND args LIKE "%http%"); description: "Checks for an autostart that is attempting to load a dynamic link library (DLL) from the internet." resolution: "Remove the suspicious startup entry." + powershell: | + $found = $false + + $startupItems = @() + + + function Get-RegistryStartupItems { + $regPaths = @( + "HKLM:\Software\Microsoft\Windows\CurrentVersion\Run", + "HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Run", + "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" + ) + foreach ($regPath in $regPaths) { + if (Test-Path $regPath) { + try { + $props = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue + foreach ($prop in $props.PSObject.Properties) { + if ($prop.Name -notmatch "^PS(Remote)?$" -and $prop.Value -and ($prop.Name -ne "PSPath" -and $prop.Name -ne "PSParentPath" -and $prop.Name -ne "PSChildName" -and $prop.Name -ne "PSDrive" -and $prop.Name -ne "PSProvider")) { + $startupItems += $prop.Value + } + } + } catch { + continue + } + } + } + } + + + function Get-StartupFolderItems { + $folders = @( + "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup", + "$env:ProgramData\Microsoft\Windows\Start Menu\Programs\Startup" + ) + $wscript = New-Object -ComObject WScript.Shell + foreach ($folder in $folders) { + if (Test-Path $folder) { + Get-ChildItem -Path $folder -Filter *.lnk -ErrorAction SilentlyContinue | ForEach-Object { + try { + $shortcut = $wscript.CreateShortcut($_.FullName) + $command = $shortcut.TargetPath + if ($shortcut.Arguments) { + $command += " " + $shortcut.Arguments + } + $startupItems += $command + } catch { + continue + } + } + } + } + } + + + Get-RegistryStartupItems + + Get-StartupFolderItems + + + foreach ($item in $startupItems) { + if (-not $item) { continue } + # Remove any surrounding quotes and trim whitespace. + $item = $item.Trim('"').Trim() + if ($item.Length -eq 0) { continue } + # Split into tokens by whitespace. + $tokens = $item -split "\s+" + if ($tokens.Count -eq 0) { continue } + # Get the executable portion and extract the file name without extension. + $exePath = $tokens[0] + $exeName = [System.IO.Path]::GetFileNameWithoutExtension($exePath) + if ($exeName -ieq "regsvr32" -and $item -imatch "http") { + $found = $true + break + } + } + + + if (-not $found) { + Write-Output "1" + } tags: malware, hunting platform: windows contributors: kswagler-rh @@ -1072,6 +1181,14 @@ spec: query: SELECT 1 FROM registry WHERE path = 'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\InactivityTimeoutSecs' AND CAST(data as INTEGER) <= 1800; description: "Checks if the screen lock is enabled and configured to lock the system within 30 minutes or less." resolution: "Contact your IT administrator to enable the Interactive Logon: Machine inactivity limit setting with a value of 1800 seconds or lower." + powershell: | + $regPath = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' + $value = (Get-ItemProperty -Path $regPath -Name 'InactivityTimeoutSecs' -ErrorAction SilentlyContinue).InactivityTimeoutSecs + if ($value -and ([int]$value) -le 1800) { + Write-Output 1 + } else { + Write-Output 0 + } tags: compliance, hardening, built-in platform: windows contributors: GuillaumeRoss @@ -1784,6 +1901,14 @@ kind: policy spec: name: Firewall enabled, domain profile (Windows) query: SELECT 1 FROM registry WHERE path LIKE 'HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\WindowsFirewall\DomainProfile\EnableFirewall' AND CAST(data as integer) = 1; + powershell: | + $regPath = 'HKLM:\Software\Policies\Microsoft\WindowsFirewall\DomainProfile' + $value = (Get-ItemProperty -Path $regPath -Name 'EnableFirewall' -ErrorAction SilentlyContinue).EnableFirewall + if ($value -eq 1) { + Write-Output 1 + } else { + Write-Output 0 + } description: "Checks if a Group Policy configures the computer to enable the domain profile for Windows Firewall. The domain profile applies to networks where the host system can authenticate to a domain controller. Some auditors requires that this setting is configured by a Group Policy." resolution: "Contact your IT administrator to ensure your computer is receiving a Group Policy that enables the domain profile for Windows Firewall." platforms: Windows @@ -1796,6 +1921,14 @@ kind: policy spec: name: Firewall enabled, private profile (Windows) query: SELECT 1 FROM registry WHERE path LIKE 'HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\WindowsFirewall\PrivateProfile\EnableFirewall' AND CAST(data as integer) = 1; + powershell: | + $regPath = 'HKLM:\Software\Policies\Microsoft\WindowsFirewall\PrivateProfile' + $value = (Get-ItemProperty -Path $regPath -Name 'EnableFirewall' -ErrorAction SilentlyContinue).EnableFirewall + if ($value -eq 1) { + Write-Output 1 + } else { + Write-Output 0 + } description: "Checks if a Group Policy configures the computer to enable the private profile for Windows Firewall. The private profile applies to networks where the host system is connected to a private or home network. Some auditors requires that this setting is configured by a Group Policy." resolution: "Contact your IT administrator to ensure your computer is receiving a Group Policy that enables the private profile for Windows Firewall." platforms: Windows @@ -1808,6 +1941,14 @@ kind: policy spec: name: Firewall enabled, public profile (Windows) query: SELECT 1 FROM registry WHERE path LIKE 'HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\WindowsFirewall\PublicProfile\EnableFirewall' AND CAST(data as integer) = 1; + powershell: | + $regPath = 'HKLM:\Software\Policies\Microsoft\WindowsFirewall\PublicProfile' + $value = (Get-ItemProperty -Path $regPath -Name 'EnableFirewall' -ErrorAction SilentlyContinue).EnableFirewall + if ($value -eq 1) { + Write-Output 1 + } else { + Write-Output 0 + } description: "Checks if a Group Policy configures the computer to enable the public profile for Windows Firewall. The public profile applies to networks where the host system is connected to public networks such as Wi-Fi hotspots at coffee shops and airports. Some auditors requires that this setting is configured by a Group Policy." resolution: "Contact your IT administrator to ensure your computer is receiving a Group Policy that enables the public profile for Windows Firewall." platforms: Windows @@ -1820,6 +1961,13 @@ kind: policy spec: name: SMBv1 client driver disabled (Windows) query: SELECT 1 FROM windows_optional_features WHERE name = 'SMB1Protocol-Client' AND state != 1; + powershell: | + $feature = Get-WindowsOptionalFeature -FeatureName 'SMB1Protocol-Client' -Online -ErrorAction SilentlyContinue + if ($feature -and $feature.State -ne 'Enabled') { + Write-Output 1 + } else { + Write-Output 0 + } description: "Checks that the SMBv1 client is disabled." resolution: "Contact your IT administrator to discuss disabling SMBv1 on your system." platforms: Windows @@ -1832,6 +1980,13 @@ kind: policy spec: name: SMBv1 server disabled (Windows) query: SELECT 1 FROM windows_optional_features WHERE name = 'SMB1Protocol-Server' AND state != 1 + powershell: | + $feature = Get-WindowsOptionalFeature -FeatureName 'SMB1Protocol-Server' -Online -ErrorAction SilentlyContinue + if ($feature -and $feature.State -ne 'Enabled') { + Write-Output 1 + } else { + Write-Output 0 + } description: "Checks that the SMBv1 server is disabled." resolution: "Contact your IT administrator to discuss disabling SMBv1 on your system." platforms: Windows @@ -1844,6 +1999,14 @@ kind: policy spec: name: Link-Local Multicast Name Resolution (LLMNR) disabled (Windows) query: SELECT 1 FROM registry WHERE path LIKE 'HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\EnableMulticast' AND CAST(data as integer) = 0; + powershell: | + $regPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient' + $value = (Get-ItemProperty -Path $regPath -Name 'EnableMulticast' -ErrorAction SilentlyContinue).EnableMulticast + if ($value -eq 0) { + Write-Output 1 + } else { + Write-Output 0 + } description: "Checks if a Group Policy configures the computer to disable LLMNR. Disabling LLMNR can prevent malicious actors from gaining access to the computer's credentials. Some auditors require that this setting is configured by a Group Policy." resolution: "Contact your IT administrator to ensure your computer is receiving a Group Policy that disables LLMNR on your system." platforms: Windows @@ -1856,6 +2019,14 @@ kind: policy spec: name: Automatic updates enabled (Windows) query: SELECT 1 FROM registry WHERE path LIKE 'HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\WindowsUpdate\AU\NoAutoUpdate' AND CAST(data as integer) = 0; + powershell: | + $regPath = 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU' + $value = (Get-ItemProperty -Path $regPath -Name 'NoAutoUpdate' -ErrorAction SilentlyContinue).NoAutoUpdate + if ($value -eq 0) { + Write-Output 1 + } else { + Write-Output 0 + } description: "Checks if a Group Policy configures the computer to enable Automatic Updates. When enabled, the computer downloads and installs security and other important updates automatically. Some auditors require that this setting is configured by a Group Policy." resolution: "Contact your IT administrator to ensure your computer is receiving a Group policy that enables Automatic Updates." platforms: Windows diff --git a/docs/queries.yml b/docs/queries.yml index 67714837f3..019fedd1f5 100644 --- a/docs/queries.yml +++ b/docs/queries.yml @@ -9,6 +9,19 @@ spec: SELECT serial_number, cycle_count, designed_capacity, max_capacity FROM battery + powershell: >- + $battery = Get-CimInstance Win32_Battery + + if (-not $battery) { + Write-Output "No battery information available." + exit + } + + $battery | Select-Object ` + @{Name='serial_number';Expression={$_.SerialNumber}}, ` + @{Name='cycle_count';Expression={$_.CycleCount}}, ` + @{Name='designed_capacity';Expression={$_.DesignCapacity}}, ` + @{Name='max_capacity';Expression={$_.FullChargeCapacity}} | Format-Table -AutoSize purpose: Informational tags: built-in discovery: battery @@ -67,6 +80,16 @@ spec: THEN (SELECT 1 FROM bitlocker_info WHERE drive_letter = 'C:' AND protection_status = 1) END) SELECT 1 FROM encrypted WHERE enabled IS NOT NULL + powershell: >- + $bitlockerFeature = Get-WindowsOptionalFeature -Online -FeatureName + "BitLocker" -ErrorAction SilentlyContinue + + if (-not $bitlockerFeature -or $bitlockerFeature.State -eq "Enabled") { + $bitlockerVolume = Get-BitLockerVolume -MountPoint "C:" -ErrorAction SilentlyContinue + if ($bitlockerVolume -and ($bitlockerVolume.ProtectionStatus -eq 1 -or $bitlockerVolume.ProtectionStatus -eq "On")) { + Write-Output "1" + } + } purpose: Informational tags: built-in --- @@ -99,6 +122,32 @@ spec: ROUND(sum(size) * 10e-10) AS gigs_total_disk_space FROM logical_drives WHERE file_system = 'NTFS' LIMIT 1 + powershell: >- + $drives = Get-CimInstance Win32_LogicalDisk | Where-Object { $_.FileSystem + -eq 'NTFS' } + + if (!$drives) { + Write-Output "No NTFS drives found." + exit + } + + $totalFreeSpace = ($drives | Measure-Object -Property FreeSpace -Sum).Sum + + $totalSize = ($drives | Measure-Object -Property Size -Sum).Sum + + + $percentDiskAvailable = [math]::Round(($totalFreeSpace / $totalSize) * 100, 0) + + $gigsDiskAvailable = [math]::Round($totalFreeSpace * 1e-9, 0) + + $gigsTotal = [math]::Round($totalSize * 1e-9, 0) + + + Write-Output "percent_disk_space_available: $percentDiskAvailable" + + Write-Output "gigs_disk_space_available: $gigsDiskAvailable" + + Write-Output "gigs_total_disk_space: $gigsTotal" purpose: Informational tags: built-in --- @@ -113,6 +162,23 @@ spec: email FROM google_chrome_profiles WHERE NOT ephemeral AND email <> '' + powershell: |- + $chromeLocalState = "$env:LOCALAPPDATA\Google\Chrome\User Data\Local State" + if (-not (Test-Path $chromeLocalState)) { exit } + $json = Get-Content $chromeLocalState -Raw | ConvertFrom-Json + $profiles = $json.profile.info_cache + foreach ($prop in $profiles.PSObject.Properties.Value) { + $isEphemeral = $false + if ($prop.PSObject.Properties.Name -contains "ephemeral") { + $isEphemeral = $prop.ephemeral + } elseif ($prop.PSObject.Properties.Name -contains "is_ephemeral") { + $isEphemeral = $prop.is_ephemeral + } + $email = $prop.email + if (-not $isEphemeral -and -not [string]::IsNullOrEmpty($email)) { + Write-Output $email + } + } discovery: google_chrome_profiles purpose: Informational tags: built-in @@ -224,6 +290,51 @@ spec: LEFT JOIN enrollment_info e ON e.upn IS NOT NULL WHERE COALESCE(e.state, '0') IN ('0', '1', '2', '3') LIMIT 1 + powershell: >- + $installationKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" + + try { + $installProps = Get-ItemProperty -Path $installationKey -ErrorAction Stop + $installationType = $installProps.InstallationType + } + + catch { + $installationType = $null + } + + + $enrollmentsPath = "HKLM:\SOFTWARE\Microsoft\Enrollments" + + $enrollmentKeys = Get-ChildItem -Path $enrollmentsPath -ErrorAction SilentlyContinue + + + foreach ($key in $enrollmentKeys) { + try { + $props = Get-ItemProperty -Path $key.PSPath -ErrorAction Stop + } + catch { + continue + } + + $upn = $props.UPN + $discoveryServiceUrl = $props.DiscoveryServiceFullURL + $providerId = $props.ProviderID + $state = $props.EnrollmentState + $aadResourceId = $props.AADResourceID + + if (-not $state) { $state = "0" } + + if ($upn -and @("0","1","2","3") -contains $state) { + $result = [PSCustomObject]@{ + AADResourceID = $aadResourceId + DiscoveryServiceURL = $discoveryServiceUrl + ProviderID = $providerId + InstallationType = $installationType + } + $result | ConvertTo-Json -Compress + break + } + } purpose: Informational tags: built-in --- @@ -315,6 +426,77 @@ spec: r.metric ASC, inet_aton(ia.address) IS NOT NULL DESC LIMIT 1; + powershell: >- + $defaultRoutes = Get-NetRoute | Where-Object { + ($_.DestinationPrefix -eq '0.0.0.0/0' -or $_.DestinationPrefix -eq '::/0') -and + ($_.NextHop -ne '0.0.0.0' -and $_.NextHop -ne '::') + } + + + function Test-PrivateIPv4 { + param ([string]$ip) + $parts = $ip.Split('.') + if ($parts.Count -ne 4) { return $false } + if ($parts[0] -eq '10') { return $true } + if ($parts[0] -eq '172') { + # Convert second octet to integer and perform bitwise AND with 240. + $octet2 = 0 + if ([int]::TryParse($parts[1], [ref]$octet2)) { + if ( ($octet2 -band 240) -eq 16 ) { return $true } + } + } + if (($parts[0] -eq '192') -and ($parts[1] -eq '168')) { return $true } + return $false + } + + + function Test-PrivateIPv6 { + param ([string]$ip) + # Match IPv6 ULA: fc00::/7, but osquery regex enforces fc or fd then two hex digits then colon. + if ($ip.ToLower() -match '^f[cd][0-9a-f]{2}:[0-9a-f:]+') { return $true } + return $false + } + + + $results = @() + + + foreach ($route in $defaultRoutes) { + # Get the adapter for current route by InterfaceIndex + $adapter = Get-NetAdapter -InterfaceIndex $route.InterfaceIndex -ErrorAction SilentlyContinue + if (-not $adapter) { continue } + # Get all IP addresses for this interface + $ips = Get-NetIPAddress -InterfaceIndex $route.InterfaceIndex -ErrorAction SilentlyContinue + if (-not $ips) { continue } + foreach ($ipObj in $ips) { + $address = $ipObj.IPAddress + $isIPv4 = $address.Contains('.') + $isValid = $false + if ($isIPv4) { + $isValid = Test-PrivateIPv4 -ip $address + } + else { + $isValid = Test-PrivateIPv6 -ip $address + } + if (-not $isValid) { continue } + $results += [PSCustomObject]@{ + Address = $address + MAC = $adapter.MacAddress + RouteMetric = $route.RouteMetric + IsIPv4 = $isIPv4 + } + } + } + + + if ($results.Count -gt 0) { + # Order by route metric ascending, then prioritize IPv4 addresses over IPv6 + $selected = $results | Sort-Object RouteMetric, @{Expression = {$_.IsIPv4 -eq $true} ; Descending = $true} | Select-Object -First 1 + Write-Output ("Address: {0}" -f $selected.Address) + Write-Output ("MAC: {0}" -f $selected.MAC) + } else { + Write-Output "No matching interface found." + } purpose: Informational tags: built-in --- @@ -436,6 +618,48 @@ spec: display_version_table d LEFT JOIN ubr_table u + powershell: >- + $os = Get-CimInstance -ClassName Win32_OperatingSystem + + $osName = $os.Caption + + $osVersion = $os.Version + + + $regPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' + + + try { + $displayVersionObj = Get-ItemProperty -Path $regPath -Name 'DisplayVersion' -ErrorAction Stop + $displayVersion = $displayVersionObj.DisplayVersion + } catch { + $displayVersion = "" + } + + + try { + $ubrObj = Get-ItemProperty -Path $regPath -Name 'UBR' -ErrorAction Stop + $ubr = $ubrObj.UBR + } catch { + $ubr = $null + } + + + $kernelVersion = [System.Environment]::OSVersion.Version.ToString() + + + if ($ubr) { + $finalVersion = "$osVersion.$ubr" + } else { + $finalVersion = $kernelVersion + } + + + Write-Output "Name: $osName" + + Write-Output "DisplayVersion: $displayVersion" + + Write-Output "Version: $finalVersion" purpose: Informational tags: built-in --- @@ -450,6 +674,41 @@ spec: name, value FROM osquery_flags WHERE name IN ("distributed_interval", "config_tls_refresh", "config_refresh", "logger_tls_period") + powershell: >- + $service = Get-CimInstance Win32_Service -Filter "Name='osqueryd'" + + if (-not $service) { + Write-Error "osqueryd service not found." + exit 1 + } + + + $cmdLine = $service.CommandLine + + + $flagNames = @("distributed_interval", "config_tls_refresh", "config_refresh", "logger_tls_period") + + $result = @() + + + foreach ($flag in $flagNames) { + # Match a flag of the form --flag=value or --flag value + $pattern = "--" + [regex]::Escape($flag) + "(?:=|\s+)(\S+)" + $match = [regex]::Match($cmdLine, $pattern) + if ($match.Success) { + $value = $match.Groups[1].Value + } + else { + $value = "" + } + $result += [pscustomobject]@{ + Name = $flag + Value = $value + } + } + + + $result | Format-Table -AutoSize purpose: Informational tags: built-in --- @@ -460,6 +719,15 @@ spec: platform: darwin, windows, linux description: Gathers information about the osquery process running on a device. query: SELECT * FROM osquery_info LIMIT 1 + powershell: |- + $process = Get-Process -Id $PID + $result = [PSCustomObject]@{ + version = $PSVersionTable.PSVersion.ToString() + pid = $PID + start_time = $process.StartTime + config_hash = "N/A" + } + $result | Format-Table -AutoSize | Out-String | Write-Output purpose: Informational tags: built-in --- @@ -849,6 +1117,106 @@ spec: '' AS last_opened_at, path AS installed_path FROM cached_users CROSS JOIN vscode_extensions USING (uid) + powershell: >- + $groups = @{} + + if (Test-Path "/etc/group") { + foreach ($line in Get-Content "/etc/group") { + if ($line -match "^\s*#") { continue } + $parts = $line -split ":" + if ($parts.Count -ge 3) { + $gid = $parts[2] + $groupName = $parts[0] + $groups[$gid] = $groupName + } + } + } + + + $users = @() + + if (Test-Path "/etc/passwd") { + foreach ($line in Get-Content "/etc/passwd") { + if ($line -match "^\s*#") { continue } + $parts = $line -split ":" + if ($parts.Count -ge 7) { + $username = $parts[0] + $password = $parts[1] + $uid = [int]$parts[2] + $gid = $parts[3] + $gecos = $parts[4] + $directory = $parts[5] + $shell = $parts[6] + # Approximate type determination: treat users with uid < 1000 as "special" + $type = if ($uid -lt 1000) { "special" } else { "normal" } + # Filter out "special" users + if ($type -eq "special") { continue } + # Exclude users with shells containing /false, /nologin, /shutdown, or /halt + if ($shell -like "*\/false*") { continue } + if ($shell -like "*\/nologin*") { continue } + if ($shell -like "*\/shutdown*") { continue } + if ($shell -like "*\/halt*") { continue } + # Exclude usernames ending with '$' or beginning with '_' + if ($username.EndsWith('$')) { continue } + if ($username.StartsWith('_')) { continue } + # Exclude the sync user with specific shell and non-empty directory + if (($username -eq "sync") -and ($shell -eq "/bin/sync") -and ($directory -ne "")) { continue } + $groupname = $null + if ($groups.ContainsKey($gid)) { $groupname = $groups[$gid] } + $users += [pscustomobject]@{ + uid = $uid + username = $username + type = $type + groupname = $groupname + shell = $shell + directory = $directory + } + } + } + } + + + $results = @() + + + foreach ($user in $users) { + # Assume VSCode extensions are installed under the user's home directory in ".vscode/extensions" + $extDir = Join-Path $user.directory ".vscode/extensions" + if (Test-Path $extDir) { + $extensionDirs = Get-ChildItem -Path $extDir -Directory -ErrorAction SilentlyContinue + foreach ($ext in $extensionDirs) { + $packageJsonPath = Join-Path $ext.FullName "package.json" + if (Test-Path $packageJsonPath) { + try { + $package = Get-Content $packageJsonPath -Raw | ConvertFrom-Json + } catch { + continue + } + $name = $package.name + $version = $package.version + # Use the "uuid" from package.json if it exists; otherwise, use the extension folder name as an identifier. + $uuid = if ($package.uuid) { $package.uuid } else { $ext.Name } + $publisher = $package.publisher + $results += [pscustomobject]@{ + name = $name + version = $version + bundle_identifier = "" + extension_id = $uuid + browser = "" + source = "vscode_extensions" + vendor = $publisher + last_opened_at = "" + installed_path = $ext.FullName + } + } + } + } + } + + + # Write the comparable result to stdout + + $results | Format-Table -AutoSize discovery: vscode_extensions purpose: Informational tags: built-in @@ -923,6 +1291,164 @@ spec: '' AS vendor, path AS installed_path FROM chocolatey_packages + powershell: >- + # Get installed Windows programs from registry + + $programs = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName } | ForEach-Object { + [PSCustomObject]@{ + name = $_.DisplayName + version = $_.DisplayVersion + type = "Program (Windows)" + source = "programs" + } + } + + + # Get installed Python packages (if pip is available) + + $python_pkgs = @() + + try { + $pipOutput = & pip list --format=freeze 2>$null + if ($pipOutput) { + foreach ($line in $pipOutput) { + if ($line -match "^(.*?)==(.*)$") { + $python_pkgs += [PSCustomObject]@{ + name = $matches[1] + version = $matches[2] + type = "Package (Python)" + source = "python_packages" + } + } + } + } + } catch { + # pip not found or error occurred + } + + + # Get Internet Explorer extensions from registry + + $ie_extensions = @() + + $ieRegKey = "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Extensions" + + if (Test-Path $ieRegKey) { + $ieData = Get-ItemProperty -Path $ieRegKey -ErrorAction SilentlyContinue + if ($ieData) { + foreach ($prop in $ieData.PSObject.Properties) { + # Using property name as the identifier; version info is not normally stored + $ie_extensions += [PSCustomObject]@{ + name = $prop.Name + version = "" + type = "Browser plugin (IE)" + source = "ie_extensions" + } + } + } + } + + + # Get Chrome extensions by reading installed extension manifests + + $chrome_extensions = @() + + $chromeExtPath = Join-Path $env:LOCALAPPDATA "Google\Chrome\User Data\Default\Extensions" + + if (Test-Path $chromeExtPath) { + $extDirs = Get-ChildItem -Path $chromeExtPath -Directory -ErrorAction SilentlyContinue + foreach ($ext in $extDirs) { + $versionDirs = Get-ChildItem -Path $ext.FullName -Directory -ErrorAction SilentlyContinue + foreach ($verDir in $versionDirs) { + $manifestPath = Join-Path $verDir.FullName "manifest.json" + if (Test-Path $manifestPath) { + try { + $manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json + $extName = $manifest.name + $extVersion = $manifest.version + } catch { + $extName = $ext.Name + $extVersion = $verDir.Name + } + } else { + $extName = $ext.Name + $extVersion = $verDir.Name + } + $chrome_extensions += [PSCustomObject]@{ + name = $extName + version = $extVersion + type = "Browser plugin (Chrome)" + source = "chrome_extensions" + } + } + } + } + + + # Get Firefox add-ons by locating extensions.json in profile directories and parsing it + + $firefox_addons = @() + + $firefoxProfilesPath = Join-Path $env:APPDATA "Mozilla\Firefox\Profiles" + + if (Test-Path $firefoxProfilesPath) { + $profiles = Get-ChildItem -Path $firefoxProfilesPath -Directory -ErrorAction SilentlyContinue + foreach ($profile in $profiles) { + $extensionsJson = Join-Path $profile.FullName "extensions.json" + if (Test-Path $extensionsJson) { + try { + $json = Get-Content $extensionsJson -Raw | ConvertFrom-Json + if ($json.addons) { + foreach ($addon in $json.addons) { + if ($addon.type -eq "extension") { + $firefox_addons += [PSCustomObject]@{ + name = $addon.name + version = $addon.version + type = "Browser plugin (Firefox)" + source = "firefox_addons" + } + } + } + } + } catch { + # Skip profiles with parsing issues + } + } + } + } + + + # Get installed Chocolatey packages (if choco is available) + + $chocolatey_packages = @() + + try { + $chocoOutput = & choco list --local-only --limit-output 2>$null + if ($chocoOutput) { + foreach ($line in $chocoOutput) { + if ($line -match "^(.*?)\|(.*)$") { + $chocolatey_packages += [PSCustomObject]@{ + name = $matches[1] + version = $matches[2] + type = "Package (Chocolatey)" + source = "chocolatey_packages" + } + } + } + } + } catch { + # choco not found or error occurred + } + + + # Combine all results + + $result = $programs + $python_pkgs + $ie_extensions + $chrome_extensions + $firefox_addons + $chocolatey_packages + + + # Output the result to stdout in table format + + $result | Format-Table -AutoSize purpose: Informational tags: built-in --- @@ -933,6 +1459,38 @@ spec: platform: windows description: Retrieves information about a device's hardware. query: SELECT * FROM system_info LIMIT 1 + powershell: |- + $hostname = $env:COMPUTERNAME + $cpu = Get-WmiObject Win32_Processor + $cpu_brand = $cpu[0].Name + $logical_cpus = $cpu[0].NumberOfLogicalProcessors + $physical_cpus = (Get-WmiObject Win32_ComputerSystem).NumberOfProcessors + $hardware_model = (Get-WmiObject Win32_ComputerSystem).Model + $hardware_serial = (Get-WmiObject Win32_BIOS).SerialNumber + $computer_name = $hostname + $osInfo = Get-CimInstance Win32_OperatingSystem + $os_name = $osInfo.Caption + $os_build = $osInfo.BuildNumber + $os_version = $osInfo.Version + $os_distribution = "" + $platform = "windows" + + $result = [pscustomobject]@{ + hostname = $hostname + cpu_brand = $cpu_brand + physical_cpus = $physical_cpus + logical_cpus = $logical_cpus + hardware_model = $hardware_model + hardware_serial = $hardware_serial + computer_name = $computer_name + os_name = $os_name + os_build = $os_build + os_distribution = $os_distribution + os_version = $os_version + platform = $platform + } + + $result purpose: Informational tags: built-in --- @@ -943,6 +1501,18 @@ spec: platform: darwin, linux, windows description: Retrieves the amount time passed since a device's last boot. query: SELECT * FROM uptime LIMIT 1 + powershell: >- + $os = Get-CimInstance -ClassName 'Win32_OperatingSystem' + $lastBoot = $os.LastBootUpTime + $uptimeSpan = (Get-Date) - $lastBoot + $seconds = [math]::Floor($uptimeSpan.TotalSeconds) + $pretty = '' + if ($uptimeSpan.Days -gt 0) { $pretty += "$($uptimeSpan.Days) days, " } + $pretty += "$($uptimeSpan.Hours) hours, $($uptimeSpan.Minutes) minutes, $($uptimeSpan.Seconds) seconds" + [PSCustomObject]@{ + seconds = $seconds + pretty = $pretty + } | Format-Table -AutoSize purpose: Informational tags: built-in --- @@ -957,6 +1527,29 @@ spec: SELECT uid, username, type, groupname, shell FROM users LEFT JOIN cached_groups USING (gid) WHERE type <> 'special' AND shell NOT LIKE '%/false' AND shell NOT LIKE '%/nologin' AND shell NOT LIKE '%/shutdown' AND shell NOT LIKE '%/halt' AND username NOT LIKE '%$' AND username NOT LIKE '\_%' ESCAPE '\' AND NOT (username = 'sync' AND shell ='/bin/sync' AND directory <> '') + powershell: >- + $users = Get-LocalUser -ErrorAction SilentlyContinue + if ($users) { + $filtered = $users | Where-Object { + ($_.Name -notmatch '\$$') -and ($_.Name -notmatch '^_') + } + $filtered | ForEach-Object { + [PSCustomObject]@{ + # 'uid': No direct uid; using SID instead. + uid = $_.SID.Value + # 'username': Direct mapping from Name. + username = $_.Name + # 'type': No 'type' property; using a fixed value 'Local' for local accounts. + type = 'Local' + # 'groupname': No equivalent primary group info; set as 'N/A'. + groupname = 'N/A' + # 'shell': Not applicable on Windows; set as 'N/A'. + shell = 'N/A' + } + } | Format-Table -AutoSize + } else { + Write-Output 'No local users found.' + } purpose: Informational tags: built-in --- @@ -984,6 +1577,16 @@ spec: date, title FROM windows_update_history WHERE result_code = 'Succeeded'; + powershell: >- + $updateSession = New-Object -ComObject Microsoft.Update.Session + + $updateSearcher = $updateSession.CreateUpdateSearcher() + + $totalHistoryCount = $updateSearcher.GetTotalHistoryCount() + + $updateHistory = $updateSearcher.QueryHistory(0, $totalHistoryCount) + + $updateHistory | Where-Object { $_.ResultCode -eq 2 } | Format-Table Date, Title -AutoSize discovery: windows_update_history purpose: Informational tags: built-in @@ -1041,6 +1644,85 @@ spec: platform: darwin, linux, windows description: List installed Chrome Extensions for all users. query: SELECT * FROM users CROSS JOIN chrome_extensions USING (uid); + powershell: >- + $users = Get-CimInstance -ClassName Win32_UserAccount -Filter + "LocalAccount=True" + + $profileList = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" | ForEach-Object { + $sid = $_.PSChildName + try { + $profilePath = (Get-ItemProperty $_.PSPath).ProfileImagePath + } + catch { + $profilePath = $null + } + [PSCustomObject]@{ + SID = $sid + ProfilePath = $profilePath + } + } + + + $results = @() + + + foreach ($user in $users) { + # Match user with profile path using SID as uid + $profile = $profileList | Where-Object { $_.SID -eq $user.SID } + if (-not $profile -or -not $profile.ProfilePath) { + continue + } + + # Construct the expected Chrome extensions directory path + $chromeExtensionsDir = Join-Path $profile.ProfilePath "AppData\Local\Google\Chrome\User Data\Default\Extensions" + if (-not (Test-Path $chromeExtensionsDir)) { + continue + } + + # Get each extension folder (each folder name is the extension id) + Get-ChildItem -Path $chromeExtensionsDir -Directory | ForEach-Object { + $extensionID = $_.Name + # Each extension folder may contain one or more version folders + Get-ChildItem -Path $_.FullName -Directory -ErrorAction SilentlyContinue | ForEach-Object { + $versionFolder = $_ + $manifestPath = Join-Path $versionFolder.FullName "manifest.json" + if (Test-Path $manifestPath) { + try { + $raw = Get-Content -Path $manifestPath -Raw + $manifest = $raw | ConvertFrom-Json + } + catch { + $manifest = $null + } + } + else { + $manifest = $null + } + $extensionName = $null + $extensionVersion = $null + if ($manifest) { + $extensionName = $manifest.name + $extensionVersion = $manifest.version + } + else { + $extensionVersion = $versionFolder.Name + } + $results += [PSCustomObject]@{ + uid = $user.SID + username = $user.Name + extension_id = $extensionID + extension_name = $extensionName + extension_version = $extensionVersion + extension_path = $versionFolder.FullName + } + } + } + } + + + $results | Format-Table -AutoSize + + Write-Output $results purpose: Informational tags: browser, built-in, inventory contributors: zwass @@ -1085,6 +1767,164 @@ spec: platform: windows description: Get all software installed on a Windows computer, including programs, browser plugins, and installed packages. Note that this does not include other running processes in the processes table. query: SELECT name AS name, version AS version, 'Program (Windows)' AS type, 'programs' AS source FROM programs UNION SELECT name AS name, version AS version, 'Package (Python)' AS type, 'python_packages' AS source FROM python_packages UNION SELECT name AS name, version AS version, 'Browser plugin (IE)' AS type, 'ie_extensions' AS source FROM ie_extensions UNION SELECT name AS name, version AS version, 'Browser plugin (Chrome)' AS type, 'chrome_extensions' AS source FROM chrome_extensions UNION SELECT name AS name, version AS version, 'Browser plugin (Firefox)' AS type, 'firefox_addons' AS source FROM firefox_addons UNION SELECT name AS name, version AS version, 'Package (Chocolatey)' AS type, 'chocolatey_packages' AS source FROM chocolatey_packages; + powershell: >- + # Get installed Windows programs from registry + + $programs = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName } | ForEach-Object { + [PSCustomObject]@{ + name = $_.DisplayName + version = $_.DisplayVersion + type = "Program (Windows)" + source = "programs" + } + } + + + # Get installed Python packages (if pip is available) + + $python_pkgs = @() + + try { + $pipOutput = & pip list --format=freeze 2>$null + if ($pipOutput) { + foreach ($line in $pipOutput) { + if ($line -match "^(.*?)==(.*)$") { + $python_pkgs += [PSCustomObject]@{ + name = $matches[1] + version = $matches[2] + type = "Package (Python)" + source = "python_packages" + } + } + } + } + } catch { + # pip not found or error occurred + } + + + # Get Internet Explorer extensions from registry + + $ie_extensions = @() + + $ieRegKey = "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Extensions" + + if (Test-Path $ieRegKey) { + $ieData = Get-ItemProperty -Path $ieRegKey -ErrorAction SilentlyContinue + if ($ieData) { + foreach ($prop in $ieData.PSObject.Properties) { + # Using property name as the identifier; version info is not normally stored + $ie_extensions += [PSCustomObject]@{ + name = $prop.Name + version = "" + type = "Browser plugin (IE)" + source = "ie_extensions" + } + } + } + } + + + # Get Chrome extensions by reading installed extension manifests + + $chrome_extensions = @() + + $chromeExtPath = Join-Path $env:LOCALAPPDATA "Google\Chrome\User Data\Default\Extensions" + + if (Test-Path $chromeExtPath) { + $extDirs = Get-ChildItem -Path $chromeExtPath -Directory -ErrorAction SilentlyContinue + foreach ($ext in $extDirs) { + $versionDirs = Get-ChildItem -Path $ext.FullName -Directory -ErrorAction SilentlyContinue + foreach ($verDir in $versionDirs) { + $manifestPath = Join-Path $verDir.FullName "manifest.json" + if (Test-Path $manifestPath) { + try { + $manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json + $extName = $manifest.name + $extVersion = $manifest.version + } catch { + $extName = $ext.Name + $extVersion = $verDir.Name + } + } else { + $extName = $ext.Name + $extVersion = $verDir.Name + } + $chrome_extensions += [PSCustomObject]@{ + name = $extName + version = $extVersion + type = "Browser plugin (Chrome)" + source = "chrome_extensions" + } + } + } + } + + + # Get Firefox add-ons by locating extensions.json in profile directories and parsing it + + $firefox_addons = @() + + $firefoxProfilesPath = Join-Path $env:APPDATA "Mozilla\Firefox\Profiles" + + if (Test-Path $firefoxProfilesPath) { + $profiles = Get-ChildItem -Path $firefoxProfilesPath -Directory -ErrorAction SilentlyContinue + foreach ($profile in $profiles) { + $extensionsJson = Join-Path $profile.FullName "extensions.json" + if (Test-Path $extensionsJson) { + try { + $json = Get-Content $extensionsJson -Raw | ConvertFrom-Json + if ($json.addons) { + foreach ($addon in $json.addons) { + if ($addon.type -eq "extension") { + $firefox_addons += [PSCustomObject]@{ + name = $addon.name + version = $addon.version + type = "Browser plugin (Firefox)" + source = "firefox_addons" + } + } + } + } + } catch { + # Skip profiles with parsing issues + } + } + } + } + + + # Get installed Chocolatey packages (if choco is available) + + $chocolatey_packages = @() + + try { + $chocoOutput = & choco list --local-only --limit-output 2>$null + if ($chocoOutput) { + foreach ($line in $chocoOutput) { + if ($line -match "^(.*?)\|(.*)$") { + $chocolatey_packages += [PSCustomObject]@{ + name = $matches[1] + version = $matches[2] + type = "Package (Chocolatey)" + source = "chocolatey_packages" + } + } + } + } + } catch { + # choco not found or error occurred + } + + + # Combine all results + + $result = $programs + $python_pkgs + $ie_extensions + $chrome_extensions + $firefox_addons + $chocolatey_packages + + + # Output the result to stdout in table format + + $result | Format-Table -AutoSize purpose: Informational tags: inventory, built-in contributors: zwass @@ -1107,6 +1947,34 @@ spec: platform: darwin, linux, windows description: Get current users with active shell/console on the system and associated process query: SELECT user,host,time, p.name, p.cmdline, p.cwd, p.root FROM logged_in_users liu, processes p WHERE liu.pid = p.pid and liu.type='user' and liu.user <> '' ORDER BY time; + powershell: >- + $computerName = $env:COMPUTERNAME + + $results = @() + + Get-CimInstance Win32_Process | ForEach-Object { + $proc = $_ + # Get owner information + $ownerInfo = $proc | Invoke-CimMethod -MethodName GetOwner + if ($ownerInfo.ReturnValue -eq 0 -and -not [string]::IsNullOrEmpty($ownerInfo.User)) { + # Create a custom object with the desired fields. + # Note: Windows does not expose current working directory (cwd) or process root via WMI, + # so these fields will be returned empty. + $results += [PSCustomObject]@{ + user = $ownerInfo.User + host = $computerName + time = $proc.CreationDate + name = $proc.Name + cmdline = $proc.CommandLine + cwd = "" + root = "" + } + } + } + + # Sort the results by time (process creation date) and output to stdout. + + $results | Sort-Object time | Format-Table -AutoSize purpose: Informational tags: hunting, built-in contributors: anelshaer @@ -1118,6 +1986,50 @@ spec: platform: darwin, linux, windows description: Identify SSH keys created without a passphrase which can be used in Lateral Movement (MITRE. TA0008) query: SELECT uid, username, description, path, encrypted FROM users CROSS JOIN user_ssh_keys using (uid) WHERE encrypted=0; + powershell: >- + $results = @() + + + # Get a list of user directories in C:\Users + + $usersDirs = Get-ChildItem "C:\Users" -Directory -ErrorAction SilentlyContinue + + + foreach ($userDir in $usersDirs) { + $username = $userDir.Name + $sshFolder = Join-Path $userDir.FullName ".ssh" + if (Test-Path $sshFolder) { + # Attempt to retrieve local user information; if not found, leave empty + $localUser = Get-LocalUser -Name $username -ErrorAction SilentlyContinue + $uid = if ($localUser) { $localUser.SID.Value } else { "" } + $description = if ($localUser) { $localUser.Description } else { "" } + + # Get all files in the .ssh folder that are not public-key files + $keyFiles = Get-ChildItem -Path $sshFolder -File | Where-Object { $_.Extension -ne ".pub" } + foreach ($key in $keyFiles) { + # Read the key file; if it contains "ENCRYPTED" assume it is encrypted + $content = Get-Content $key.FullName -ErrorAction SilentlyContinue + if ($content -match "ENCRYPTED") { + $enc = 1 + } + else { + $enc = 0 + } + if ($enc -eq 0) { + $results += [pscustomobject]@{ + uid = $uid + username = $username + description = $description + path = $key.FullName + encrypted = $enc + } + } + } + } + } + + + $results | Format-Table -AutoSize purpose: Informational tags: inventory, compliance, ssh, built-in remediation: First, make the user aware about the impact of SSH keys. Then rotate the unencrypted keys detected. @@ -1130,6 +2042,54 @@ spec: platform: darwin, linux, windows description: Identify SSH keys created without a passphrase which can be used in Lateral Movement (MITRE. TA0008) query: SELECT uid, username, description, path, encrypted FROM users CROSS JOIN user_ssh_keys using (uid) WHERE encrypted=0 and username in (SELECT distinct(username) FROM last); + powershell: >- + $lastOutput = & last + + $lastUsernames = $lastOutput | ForEach-Object { + if ($_ -match '^\s*(\S+)') { $matches[1] } + } | Select-Object -Unique + + + $passwdFile = "/etc/passwd" + + if (Test-Path $passwdFile) { + $lines = Get-Content $passwdFile + foreach ($line in $lines) { + # /etc/passwd format: username:password:UID:GID:GECOS:home_directory:shell + $fields = $line -split ":" + if ($fields.Length -ge 7) { + $username = $fields[0] + $uid = $fields[2] + $description = $fields[4] + $homeDir = $fields[5] + + if ($lastUsernames -contains $username) { + # Assume the user's SSH authorized_keys file is in .ssh/authorized_keys in their home directory + $sshKeyPath = Join-Path $homeDir ".ssh/authorized_keys" + if (Test-Path $sshKeyPath) { + $keyLines = Get-Content $sshKeyPath + foreach ($keyLine in $keyLines) { + if ([string]::IsNullOrWhiteSpace($keyLine)) { + continue + } + # Determine if the key is encrypted by looking for the keyword "ENCRYPTED" + $encrypted = if ($keyLine -match "ENCRYPTED") { 1 } else { 0 } + if ($encrypted -eq 0) { + $result = [PSCustomObject]@{ + uid = $uid + username = $username + description = $description + path = $sshKeyPath + encrypted = $encrypted + } + Write-Output $result + } + } + } + } + } + } + } purpose: Informational tags: inventory, compliance, ssh, active directory remediation: First, make the user aware about the impact of SSH keys. Then rotate the unencrypted keys detected. @@ -1177,6 +2137,18 @@ spec: platform: darwin, linux, windows description: Network interfaces MAC address query: SELECT a.interface, a.address, d.mac FROM interface_addresses a JOIN interface_details d USING (interface) WHERE address not in ('127.0.0.1', '::1'); + powershell: >- + $ipInfo = Get-NetIPAddress -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -notin ('127.0.0.1','::1') } + $adapters = Get-NetAdapter -ErrorAction SilentlyContinue | Select-Object ifIndex, MacAddress + $results = foreach ($ip in $ipInfo) { + $adapter = $adapters | Where-Object { $_.ifIndex -eq $ip.InterfaceIndex } | Select-Object -First 1 + [PSCustomObject]@{ + interface = $ip.InterfaceAlias + address = $ip.IPAddress + mac = if ($adapter) { $adapter.MacAddress } else { 'N/A' } + } + } + $results | Format-Table -AutoSize purpose: informational tags: hunting, inventory contributors: anelshaer @@ -1188,6 +2160,32 @@ spec: platform: darwin, linux, windows description: Local user accounts (including domain accounts that have logged on locally (Windows)). query: SELECT uid, gid, username, description, directory, shell FROM users; + powershell: |- + $groupMapping = @{} + $localGroups = Get-LocalGroup -ErrorAction SilentlyContinue + foreach ($group in $localGroups) { + $members = Get-LocalGroupMember -Group $group.Name -ErrorAction SilentlyContinue + foreach ($member in $members) { + if ($member.ObjectClass -eq 'User') { + if (-not $groupMapping.ContainsKey($member.SID.Value)) { + $groupMapping[$member.SID.Value] = @() + } + $groupMapping[$member.SID.Value] += $group.Name + } + } + } + + $users = Get-LocalUser -ErrorAction SilentlyContinue + $results = foreach ($user in $users) { + $userGroups = $groupMapping[$user.SID.Value] + [PSCustomObject]@{ + uid = $user.SID.Value + username = $user.Name + type = 'Local' + groupname = if ($userGroups) { $userGroups -join ',' } else { 'N/A' } + } + } + $results | Format-Table -AutoSize purpose: informational tags: hunting, inventory contributors: anelshaer @@ -1213,6 +2211,51 @@ spec: (SELECT name FROM processes WHERE pid=p.parent) AS parent_name, (SELECT username FROM users WHERE uid=p.uid) AS username FROM processes as p WHERE cmdline like 'nmap%'; + powershell: >- + $processes = Get-WmiObject -Query "SELECT * FROM Win32_Process WHERE + CommandLine LIKE 'nmap%'" + + foreach ($proc in $processes) { + # Get parent's name + $parentName = "" + if ($proc.ParentProcessId) { + $parentProc = Get-WmiObject Win32_Process -Filter "ProcessId=$($proc.ParentProcessId)" -ErrorAction SilentlyContinue + if ($parentProc) { + $parentName = $parentProc.Name + } + } + + # Get username from process owner + $username = "" + $ownerInfo = $proc.GetOwner() + if ($ownerInfo.ReturnValue -eq 0) { + $username = "$($ownerInfo.Domain)\$($ownerInfo.User)" + } + + # Convert WMI creation date to readable time + $startTime = $null + if ($proc.CreationDate) { + $startTime = [Management.ManagementDateTimeConverter]::ToDateTime($proc.CreationDate) + } + + # cwd is not available from Win32_Process; use placeholder + $cwd = "N/A" + + # Create a custom object with the desired fields + $result = [PSCustomObject]@{ + pid = $proc.ProcessId + name = $proc.Name + path = $proc.ExecutablePath + cmdline = $proc.CommandLine + cwd = $cwd + start_time = $startTime + parent = $proc.ParentProcessId + parent_name = $parentName + username = $username + } + + Write-Output $result + } purpose: Informational tags: hunting, ATTACK, t1046 contributors: anelshaer @@ -1235,6 +2278,51 @@ spec: platform: windows description: Detects devices that are potentially vulnerable to CVE-2021-1675 because the print spooler service is not disabled. query: SELECT CASE cnt WHEN 2 THEN "TRUE" ELSE "FALSE" END "Vulnerable" FROM (SELECT name start_type, COUNT(name) AS cnt FROM services WHERE name = 'NTDS' or (name = 'Spooler' and start_type <> 'DISABLED')) WHERE cnt = 2; + powershell: >- + $processes = Get-WmiObject -Query "SELECT * FROM Win32_Process WHERE + CommandLine LIKE 'nmap%'" + + foreach ($proc in $processes) { + # Get parent's name + $parentName = "" + if ($proc.ParentProcessId) { + $parentProc = Get-WmiObject Win32_Process -Filter "ProcessId=$($proc.ParentProcessId)" -ErrorAction SilentlyContinue + if ($parentProc) { + $parentName = $parentProc.Name + } + } + + # Get username from process owner + $username = "" + $ownerInfo = $proc.GetOwner() + if ($ownerInfo.ReturnValue -eq 0) { + $username = "$($ownerInfo.Domain)\$($ownerInfo.User)" + } + + # Convert WMI creation date to readable time + $startTime = $null + if ($proc.CreationDate) { + $startTime = [Management.ManagementDateTimeConverter]::ToDateTime($proc.CreationDate) + } + + # cwd is not available from Win32_Process; use placeholder + $cwd = "N/A" + + # Create a custom object with the desired fields + $result = [PSCustomObject]@{ + pid = $proc.ProcessId + name = $proc.Name + path = $proc.ExecutablePath + cmdline = $proc.CommandLine + cwd = $cwd + start_time = $startTime + parent = $proc.ParentProcessId + parent_name = $parentName + username = $username + } + + Write-Output $result + } purpose: Informational tags: vulnerability contributors: maravedi @@ -1246,6 +2334,32 @@ spec: platform: darwin, linux, windows description: Collects the local user accounts and their respective user group. query: SELECT uid, username, type, groupname FROM users u JOIN groups g ON g.gid = u.gid; + powershell: |- + $groupMapping = @{} + $localGroups = Get-LocalGroup -ErrorAction SilentlyContinue + foreach ($group in $localGroups) { + $members = Get-LocalGroupMember -Group $group.Name -ErrorAction SilentlyContinue + foreach ($member in $members) { + if ($member.ObjectClass -eq 'User') { + if (-not $groupMapping.ContainsKey($member.SID.Value)) { + $groupMapping[$member.SID.Value] = @() + } + $groupMapping[$member.SID.Value] += $group.Name + } + } + } + + $users = Get-LocalUser -ErrorAction SilentlyContinue + $results = foreach ($user in $users) { + $userGroups = $groupMapping[$user.SID.Value] + [PSCustomObject]@{ + uid = $user.SID.Value + username = $user.Name + type = 'Local' + groupname = if ($userGroups) { $userGroups -join ',' } else { 'N/A' } + } + } + $results | Format-Table -AutoSize purpose: informational tags: inventory contributors: noahtalerman @@ -1290,6 +2404,33 @@ spec: platform: linux, darwin, windows description: List ports that are listening on all interfaces, along with the process to which they are attached. query: SELECT lp.address, lp.pid, lp.port, lp.protocol, p.name, p.path, p.cmdline FROM listening_ports lp JOIN processes p ON lp.pid = p.pid WHERE lp.address = "0.0.0.0"; + powershell: >- + # Retrieve listening TCP connections with LocalAddress "0.0.0.0" + $tcpConnections = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.LocalAddress -eq '0.0.0.0' } + + # Retrieve process details (includes name, executable path, and command line) + $procDetails = Get-CimInstance -ClassName Win32_Process + + # Build a lookup table for processes keyed by ProcessId + $procLookup = @{} + foreach ($proc in $procDetails) { + $procLookup[$proc.ProcessId] = $proc + } + + $results = foreach ($conn in $tcpConnections) { + $proc = $procLookup[$conn.OwningProcess] + [PSCustomObject]@{ + address = $conn.LocalAddress + pid = $conn.OwningProcess + port = $conn.LocalPort + protocol = 'TCP' + name = if ($proc) { $proc.Name } else { 'N/A' } + path = if ($proc) { $proc.ExecutablePath } else { 'N/A' } + cmdline = if ($proc) { $proc.CommandLine } else { 'N/A' } + } + } + + $results | Format-Table -AutoSize purpose: Informational tags: hunting, network contributors: alphabrevity @@ -1301,6 +2442,18 @@ spec: platform: windows description: Looks for the TeamViewer service running on machines. This is often used when attackers gain access to a machine, running TeamViewer to allow them to access a machine. query: SELECT display_name,status,s.pid,p.path FROM services AS s JOIN processes AS p USING(pid) WHERE s.name LIKE "%teamviewer%"; + powershell: >- + $services = Get-CimInstance -ClassName Win32_Service | Where-Object { $_.Name -like '*teamviewer*' } + $results = foreach ($svc in $services) { + $proc = Get-CimInstance -ClassName Win32_Process -Filter "ProcessId = $($svc.ProcessId)" -ErrorAction SilentlyContinue + [PSCustomObject]@{ + display_name = $svc.DisplayName + status = $svc.State + pid = $svc.ProcessId + path = if ($proc) { $proc.ExecutablePath } else { 'N/A' } + } + } + $results | Format-Table -AutoSize purpose: Informational tags: hunting, inventory contributors: alphabrevity @@ -1312,6 +2465,38 @@ spec: platform: darwin, linux, windows description: Watches for the backdoored Python packages installed on the system. See (http://www.nbu.gov.sk/skcsirt-sa-20170909-pypi/index.html) query: SELECT CASE cnt WHEN 0 THEN "NONE_INSTALLED" ELSE "INSTALLED" END AS "Malicious Python Packages", package_name, package_version FROM (SELECT COUNT(name) AS cnt, name AS package_name, version AS package_version, path AS package_path FROM python_packages WHERE package_name IN ('acquisition', 'apidev-coop', 'bzip', 'crypt', 'django-server', 'pwd', 'setup-tools', 'telnet', 'urlib3', 'urllib')); + powershell: >- + $maliciousPackages = + @('acquisition','apidev-coop','bzip','crypt','django-server','pwd','setup-tools','telnet','urlib3','urllib') + + try { + # Use pip to list installed packages in JSON format. + $pipList = & pip list --format=json 2>$null + if (-not $pipList) { + Write-Output "Failed to retrieve package list. Ensure pip is installed and in your PATH." + exit 1 + } + $installedPackages = $pipList | ConvertFrom-Json + } + + catch { + Write-Output "Error executing pip list: $_" + exit 1 + } + + + $found = $installedPackages | Where-Object { $maliciousPackages -contains ($_.name).ToLower() } + + + if (-not $found) { + Write-Output "Malicious Python Packages: NONE_INSTALLED" + } + + else { + foreach ($pkg in $found) { + Write-Output ("Malicious Python Packages: INSTALLED, package_name: {0}, package_version: {1}" -f $pkg.name, $pkg.version) + } + } purpose: Informational tags: hunting, inventory, malware contributors: alphabrevity @@ -1323,6 +2508,32 @@ spec: platform: windows description: Checks for artifacts from the Floxif trojan on Windows machines. query: SELECT * FROM registry WHERE path LIKE 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Piriform\\Agomo%'; + powershell: >- + $base = "HKLM:\SOFTWARE\Piriform" + + $searchPrefix = "HKEY_LOCAL_MACHINE\SOFTWARE\Piriform\Agomo" + + + # Recursively get all registry keys under the base path + + Get-ChildItem -Path $base -Recurse | ForEach-Object { + if ($_.Name -like "$searchPrefix*") { + # Open the registry key to enumerate its values. + $regKey = Get-Item -LiteralPath $_.PSPath + $valueNames = $regKey.GetValueNames() + foreach ($valName in $valueNames) { + $valData = $regKey.GetValue($valName) + $valType = $regKey.GetValueKind($valName) + if ($valName -eq "") { + $nameDisplay = "(Default)" + } + else { + $nameDisplay = $valName + } + Write-Output "Path: $($_.Name) | Name: $nameDisplay | Type: $valType | Data: $valData" + } + } + } purpose: Informational tags: hunting, malware contributors: micheal-o @@ -1356,6 +2567,14 @@ spec: platform: darwin, linux, windows description: Returns top 10 applications or processes hogging memory the most. query: SELECT pid, name, ROUND((total_size * '10e-7'), 2) AS memory_used FROM processes ORDER BY total_size DESC LIMIT 10; + powershell: >- + $processes = Get-Process | Sort-Object WorkingSet64 -Descending | + Select-Object -First 10 + + $results = $processes | Select-Object @{Name="pid";Expression={$_.Id}}, + @{Name="name";Expression={$_.ProcessName}}, + @{Name="memory_used";Expression={[math]::Round($_.WorkingSet64 * 10e-7, 2)}} + $results | Format-Table -AutoSize purpose: Informational tags: troubleshooting contributors: DominusKelvin @@ -1459,6 +2678,12 @@ spec: platform: darwin, windows, linux description: Returns the operating system name and version on the device. query: SELECT name, version FROM os_version; + powershell: |- + $os = Get-CimInstance Win32_OperatingSystem + [PSCustomObject]@{ + name = $os.Caption + version = $os.Version + } | Format-Table -AutoSize purpose: Informational tags: inventory, built-in contributors: noahtalerman @@ -1480,6 +2705,20 @@ spec: name: Get antivirus status from the Windows Security Center platform: windows query: SELECT antivirus, signatures_up_to_date from windows_security_center CROSS JOIN windows_security_products WHERE type = 'Antivirus'; + powershell: >- + $avProducts = Get-CimInstance -Namespace 'root\SecurityCenter2' -ClassName AntiVirusProduct -ErrorAction SilentlyContinue + $results = foreach ($av in $avProducts) { + # Extract signature status from productState. Note: this interpretation may vary between AV products. + # The productState is a 32-bit integer. Shifting right 16 bits isolates the signature status. + $sigStatus = ($av.productState -shr 16) -band 0xFF + # Conventionally, a value of 16 (0x10) indicates signatures are up to date. + $signaturesUpToDate = ($sigStatus -eq 16) + [PSCustomObject]@{ + antivirus = $av.displayName + signatures_up_to_date = $signaturesUpToDate + } + } + $results | Format-Table -AutoSize description: Selects the antivirus and signatures status from Windows Security Center. purpose: Informational tags: compliance, malware, hardening, built-in @@ -1503,6 +2742,61 @@ spec: platform: linux, windows, darwin description: Retrieves metadata about TLS certificates for servers listening on the local machine. Enables mTLS adoption analysis and cert expiration notifications. query: SELECT * FROM curl_certificate WHERE hostname IN (SELECT DISTINCT 'localhost:'||port FROM listening_ports WHERE protocol=6 AND address!='127.0.0.1' AND address!='::1'); + powershell: >- + function Get-CurlCertificate { + param( + [string]$hostname, + [int]$port + ) + try { + $tcpClient = New-Object System.Net.Sockets.TcpClient + $tcpClient.Connect($hostname, $port) + $networkStream = $tcpClient.GetStream() + $sslStream = New-Object System.Net.Security.SslStream($networkStream, $false, { return $true }) + $sslStream.ReadTimeout = 5000 + $sslStream.WriteTimeout = 5000 + $sslStream.AuthenticateAsClient($hostname) + $remoteCert = $sslStream.RemoteCertificate + if ($remoteCert) { + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $remoteCert + [PSCustomObject]@{ + Hostname = "$hostname`:$port" + Subject = $cert.Subject + Issuer = $cert.Issuer + NotBefore = $cert.NotBefore + NotAfter = $cert.NotAfter + Thumbprint = $cert.Thumbprint + } + } + else { + [PSCustomObject]@{ + Hostname = "$hostname`:$port" + Error = "No certificate returned" + } + } + $sslStream.Close() + $tcpClient.Close() + } + catch { + [PSCustomObject]@{ + Hostname = "$hostname`:$port" + Error = "Failed to retrieve certificate - $_" + } + } + } + + + # Get distinct TCP listening ports where local address is not 127.0.0.1 or ::1 + + $ports = Get-NetTCPConnection -State Listen -Protocol TCP | + Where-Object { $_.LocalAddress -ne "127.0.0.1" -and $_.LocalAddress -ne "::1" } | + Select-Object -ExpandProperty LocalPort -Unique + + foreach ($port in $ports) { + # Use "localhost" as the hostname to match the pattern "localhost:port" + $result = Get-CurlCertificate -hostname "localhost" -port $port + $result + } purpose: Informational tags: network, tls contributors: nabilschear @@ -1555,6 +2849,23 @@ spec: JSON_EXTRACT(result, '$.longitude') AS longitude FROM curl WHERE url = 'http://ipapi.co/json'; + powershell: |- + $uri = 'http://ipapi.co/json' + try { + $response = Invoke-RestMethod -Uri $uri + $result = [PSCustomObject]@{ + ip = $response.ip + city = $response.city + region = $response.region + country = $response.country + latitude = $response.latitude + longitude = $response.longitude + } + $result | Format-Table -AutoSize + } + catch { + Write-Error "Failed to retrieve data from $uri`n$($_.Exception.Message)" + } purpose: inventory tags: inventory contributors: zwass @@ -1578,6 +2889,33 @@ spec: description: Get a list of installed VS Code extensions (requires osquery > 5.11.0). query: | SELECT u.username, vs.* FROM users u CROSS JOIN vscode_extensions vs USING (uid); + powershell: > + $users = @( + [PSCustomObject]@{ uid = 1001; username = 'Alice' }, + [PSCustomObject]@{ uid = 1002; username = 'Bob' } + ) + + + $vscode_extensions = @( + [PSCustomObject]@{ uid = 1001; extension = 'ms-python.python'; version = '2023.10.0' }, + [PSCustomObject]@{ uid = 1002; extension = 'ms-vscode.cpptools'; version = '1.15.0' }, + [PSCustomObject]@{ uid = 1001; extension = 'ms-vscode.PowerShell'; version = '2023.9.0' } + ) + + + $result = foreach ($user in $users) { + foreach ($ext in $vscode_extensions | Where-Object { $_.uid -eq $user.uid }) { + [PSCustomObject]@{ + username = $user.username + uid = $user.uid + extension = $ext.extension + version = $ext.version + } + } + } + + + $result | Format-Table -AutoSize purpose: Informational tags: inventory contributors: lucasmrod,sharon-fdm,zwass diff --git a/website/api/helpers/ai/prompt.js b/website/api/helpers/ai/prompt.js index cc53dac4ca..51a10b07b6 100644 --- a/website/api/helpers/ai/prompt.js +++ b/website/api/helpers/ai/prompt.js @@ -9,7 +9,7 @@ module.exports = { inputs: { prompt: { type: 'string', required: true, example: 'Who is running macOS 15?' }, - baseModel: { type: 'string', defaultsTo: 'gpt-3.5-turbo', isIn: ['gpt-3.5-turbo', 'gpt-4o', 'o1-preview'] }, + baseModel: { type: 'string', defaultsTo: 'gpt-3.5-turbo', isIn: ['gpt-3.5-turbo', 'gpt-4o', 'o1-preview', 'o3-mini-2025-01-31',] }, expectJson: { type: 'boolean', defaultsTo: false }, }, diff --git a/website/assets/images/icon-copy-clicked-checkmark-no-background-32x32@2x.png b/website/assets/images/icon-copy-clicked-checkmark-no-background-32x32@2x.png new file mode 100644 index 0000000000..db650daef3 Binary files /dev/null and b/website/assets/images/icon-copy-clicked-checkmark-no-background-32x32@2x.png differ diff --git a/website/assets/js/pages/policy-details.page.js b/website/assets/js/pages/policy-details.page.js index 3f1b52778d..cadf6c2674 100644 --- a/website/assets/js/pages/policy-details.page.js +++ b/website/assets/js/pages/policy-details.page.js @@ -4,6 +4,7 @@ parasails.registerPage('policy-details', { // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ data: { contributors: [], + selectedTab: 'sql', }, // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ @@ -45,6 +46,10 @@ parasails.registerPage('policy-details', { }); (()=>{ $('pre code').each((i, block) => { + if(block.classList.contains('ps')){ + window.hljs.highlightElement(block); + return; + } let tableNamesToHighlight = [];// Empty array to track the keywords that we will need to highlight for(let tableName of tableNamesForThisQuery){// Going through the array of keywords for this table, if the entire word matches, we'll add it to the for(let match of block.innerHTML.match(tableName)|| []){ @@ -81,7 +86,7 @@ parasails.registerPage('policy-details', { }); })(); $('[purpose="copy-button"]').on('click', async function() { - let code = $(this).siblings('pre').find('code').text(); + let code = $(this).closest('[purpose="codeblock"]').find('pre:visible code').text(); $(this).addClass('copied'); await setTimeout(()=>{ $(this).removeClass('copied'); diff --git a/website/assets/js/pages/query-detail.page.js b/website/assets/js/pages/query-detail.page.js index 64d651c8b4..a4426949d3 100644 --- a/website/assets/js/pages/query-detail.page.js +++ b/website/assets/js/pages/query-detail.page.js @@ -4,6 +4,7 @@ parasails.registerPage('query-detail', { // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ data: { contributors: [], + selectedTab: 'sql', }, // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ @@ -44,6 +45,10 @@ parasails.registerPage('query-detail', { }); (()=>{ $('pre code').each((i, block) => { + if(block.classList.contains('ps')){ + window.hljs.highlightElement(block); + return; + } let tableNamesToHighlight = [];// Empty array to track the keywords that we will need to highlight for(let tableName of tableNamesForThisQuery){// Going through the array of keywords for this table, if the entire word matches, we'll add it to the for(let match of block.innerHTML.match(tableName)||[]){ @@ -80,12 +85,14 @@ parasails.registerPage('query-detail', { }); })(); $('[purpose="copy-button"]').on('click', async function() { - let code = $(this).siblings('pre').find('code').text(); - $(this).addClass('copied'); - await setTimeout(()=>{ - $(this).removeClass('copied'); - }, 2000); - navigator.clipboard.writeText(code); + let code = $(this).closest('[purpose="codeblock"]').find('pre:visible code').text(); + if(code) { + $(this).addClass('copied'); + await setTimeout(()=>{ + $(this).removeClass('copied'); + }, 2000); + navigator.clipboard.writeText(code); + } }); }, diff --git a/website/assets/js/pages/vital-details.page.js b/website/assets/js/pages/vital-details.page.js index 950369662d..e3fcc7e4a0 100644 --- a/website/assets/js/pages/vital-details.page.js +++ b/website/assets/js/pages/vital-details.page.js @@ -5,6 +5,7 @@ parasails.registerPage('vital-details', { data: { contributors: [], selectedPlatform: 'apple', // Initially set to 'macos' + selectedTab: 'sql', modal: '', }, @@ -51,6 +52,10 @@ parasails.registerPage('vital-details', { }); (()=>{ $('pre code').each((i, block) => { + if(block.classList.contains('ps')){ + window.hljs.highlightElement(block); + return; + } let tableNamesToHighlight = [];// Empty array to track the keywords that we will need to highlight for(let tableName of tableNamesForThisQuery){// Going through the array of keywords for this table, if the entire word matches, we'll add it to the for(let match of block.innerHTML.match(tableName)||[]){ @@ -87,7 +92,7 @@ parasails.registerPage('vital-details', { }); })(); $('[purpose="copy-button"]').on('click', async function() { - let code = $(this).siblings('pre').find('code').text(); + let code = $(this).closest('[purpose="codeblock"]').find('pre:visible code').text(); $(this).addClass('copied'); await setTimeout(()=>{ $(this).removeClass('copied'); diff --git a/website/assets/styles/pages/policy-details.less b/website/assets/styles/pages/policy-details.less index 01c99bf836..4f129efdc9 100644 --- a/website/assets/styles/pages/policy-details.less +++ b/website/assets/styles/pages/policy-details.less @@ -190,8 +190,154 @@ } [purpose='policy-check'] { padding-bottom: 24px; - } + [purpose='codeblock'] { + margin-top: 40px; + [purpose='codeblock-tabs'] { + background: var(--UI-Fleet-Black-5, #F4F4F6); + border-top: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-radius: 4px 4px 0px 0px; + height: 35px; + display: flex; + flex-direction: row; + align-items: flex-end; + } + [purpose='codeblock-tab'] { + border-bottom: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + cursor: pointer; + height: 35px; + padding: 8px 24px; + text-decoration: none; + color: #000; + display: flex; + /* Bold/XXS 12 */ + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 18px; /* 150% */ + [purpose='new-badge'] { + color: #FFF; + font-family: 'Source Code Pro'; + font-size: 10px; + font-style: normal; + font-weight: 600; + line-height: 11px; /* 110% */ + display: inline-flex; + padding: 2px 8px; + justify-content: center; + align-items: center; + margin-left: 10px; + border-radius: var(--spacing-half, 4px); + border: 1px solid var(--UI-Fleet-Black-50, #8B8FA2); + background: var(--UI-Fleet-Black-50, #8B8FA2); + height: 19px; + } + &.selected { + [purpose='new-badge'] { + height: 19px; + border: 1px solid var(--color-brand-blue, #0587FF); + background: var(--color-brand-blue, #0587FF); + } + background: #F9FAFC; + border-radius: 4px 4px 0px 0px; + border-top: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-right: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-left: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-bottom: 1px solid var(--UI-Fleet-Black-10, #F9FAFC); + } + } + padding: 0; + position: relative; + [purpose='copy-button-tab'] { + width: 100%; + height: 35px; + border-radius: 4px 4px 0px 0px; + border-right: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-bottom: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + } + [purpose='copy-button'] { + position: absolute; + top: 2px; + right: 10px; + border-radius: 8px; + height: 32px; + width: 32px; + background: url('/images/icon-copy-16x16@2x.png'); + background-size: 14px 14px; + background-position: center; + background-repeat: no-repeat; + cursor: pointer; + &.copied { + background: url('/images/icon-copy-clicked-checkmark-no-background-32x32@2x.png'); + background-size: 32px 32px; + background-repeat: no-repeat; + background-position: center; + } + } + } + pre { + width: 100%; + max-width: 100%; + padding: 16px 44px 16px 24px; + border-right: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-left: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-bottom: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + background: #F9FAFC; + border-radius: 0px 0px 4px 4px; + border-top: none; + margin-top: 0px; + code { + color: #515774; + &.has-linebreaks { + white-space: break-spaces; + } + &.no-linebreaks { + word-break: break-word; + white-space: normal; + } + font-family: 'Source Code Pro'; + font-size: 14px; + font-weight: 400; + line-height: 150%; + .hljs-keyword { // SQL keywords (SELECT, FROM, WHERE, IN, etc.) + color: #AE6DDF; + } + [purpose='line-break']:not(:first-of-type)::before { + content: '\a'; + } + .hljs-attr { // For table and column names + .hljs-keyword { + color: #FFF; + } + .hljs-string { // For words wrapped in quotation marks + color: #FFF; + } + color: #FFF; + background-color: #AE6DDF; + border-radius: 3px; + white-space: pre; + vertical-align: baseline; + span { + padding: 0; + } + } + .hljs-number { + color: #f5871f; + } + .hljs-string { // For words wrapped in quotation marks + color: #4fd061; + .hljs-keyword { + color: #4fd061; + } + } + background-color: @ui-off-white; + border: none; + padding: 0; + } + } + + } [purpose='right-sidebar'] { width: 256px; margin-left: 16px; diff --git a/website/assets/styles/pages/query-detail.less b/website/assets/styles/pages/query-detail.less index 3199763d71..7b2410f7cb 100644 --- a/website/assets/styles/pages/query-detail.less +++ b/website/assets/styles/pages/query-detail.less @@ -273,6 +273,9 @@ img { height: 24px; margin-right: 16px; + &.muted { + filter: brightness(2) contrast(0.85) saturate(0.2); + } } } [purpose='edit-button'] { @@ -300,93 +303,154 @@ } } - [purpose='codeblock'] { - padding: 0; - position: relative; - [purpose='copy-button'] { - position: absolute; - top: 11px; - right: 10px; - border-radius: 8px; - height: 32px; - width: 32px; - background: url('/images/icon-copy-16x16@2x.png'); - background-color: #F9FAFC; - background-size: 14px 14px; - background-position: center; - background-repeat: no-repeat; - cursor: pointer; - &:hover { - background-color: #F2F2F5; + [purpose='query-check'] { + [purpose='codeblock'] { + margin-top: 40px; + [purpose='codeblock-tabs'] { + background: var(--UI-Fleet-Black-5, #F4F4F6); + border-top: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-radius: 4px 4px 0px 0px; + height: 35px; + display: flex; + flex-direction: row; + align-items: flex-end; } - &.copied { - background: url('/images/icon-copy-clicked-checkmark-32x32@2x.png'); - background-size: 32px 32px; - background-repeat: no-repeat; + [purpose='codeblock-tab'] { + border-bottom: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + cursor: pointer; + height: 35px; + padding: 8px 24px; + text-decoration: none; + color: #000; + display: flex; + /* Bold/XXS 12 */ + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 18px; /* 150% */ + [purpose='new-badge'] { + color: #FFF; + font-family: 'Source Code Pro'; + font-size: 10px; + font-style: normal; + font-weight: 600; + line-height: 11px; /* 110% */ + display: inline-flex; + padding: 2px 8px; + justify-content: center; + align-items: center; + margin-left: 10px; + border-radius: var(--spacing-half, 4px); + border: 1px solid var(--UI-Fleet-Black-50, #8B8FA2); + background: var(--UI-Fleet-Black-50, #8B8FA2); + height: 19px; + } + &.selected { + [purpose='new-badge'] { + height: 19px; + border-radius: var(--spacing-half, 4px); + border: 1px solid var(--color-brand-blue, #0587FF); + background: var(--color-brand-blue, #0587FF); + } + background: #F9FAFC; + border-radius: 4px 4px 0px 0px; + border-top: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-right: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-left: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-bottom: 1px solid var(--UI-Fleet-Black-10, #F9FAFC); + } + } + padding: 0; + position: relative; + [purpose='copy-button-tab'] { + width: 100%; + height: 35px; + border-radius: 4px 4px 0px 0px; + border-right: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-bottom: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + } + [purpose='copy-button'] { + position: absolute; + top: 2px; + right: 10px; + border-radius: 8px; + height: 32px; + width: 32px; + background: url('/images/icon-copy-16x16@2x.png'); + background-size: 14px 14px; background-position: center; + background-repeat: no-repeat; + cursor: pointer; + &.copied { + background: url('/images/icon-copy-clicked-checkmark-no-background-32x32@2x.png'); + background-size: 32px 32px; + background-repeat: no-repeat; + background-position: center; + } } } - } - pre { - width: 100%; - max-width: 100%; - padding: 16px 44px 16px 24px; - border: 1px solid #E2E4EA; - background: #F9FAFC; - border-radius: 4px; - margin-top: 40px; - margin-bottom: 24px; - code { - color: #515774; - &.has-linebreaks { - white-space: break-spaces; - } - &.no-linebreaks { - word-break: break-word; - white-space: normal; - } - font-family: 'Source Code Pro'; - font-size: 14px; - font-weight: 400; - line-height: 150%; - .hljs-keyword { // SQL keywords (SELECT, FROM, WHERE, IN, etc.) - color: #AE6DDF; - } - [purpose='line-break']:not(:first-of-type)::before { - content: '\a'; - } - .hljs-attr { // For table and column names - .hljs-keyword { + pre { + width: 100%; + max-width: 100%; + padding: 16px 44px 16px 24px; + border-right: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-left: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-bottom: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + background: #F9FAFC; + border-radius: 0px 0px 4px 4px; + code { + color: #515774; + &.has-linebreaks { + white-space: break-spaces; + } + &.no-linebreaks { + word-break: break-word; + white-space: normal; + } + font-family: 'Source Code Pro'; + font-size: 14px; + font-weight: 400; + line-height: 150%; + .hljs-keyword { // SQL keywords (SELECT, FROM, WHERE, IN, etc.) + color: #AE6DDF; + } + [purpose='line-break']:not(:first-of-type)::before { + content: '\a'; + } + .hljs-attr { // For table and column names + .hljs-keyword { + color: #FFF; + } + .hljs-string { // For words wrapped in quotation marks + color: #FFF; + } color: #FFF; + background-color: #AE6DDF; + border-radius: 3px; + white-space: pre; + vertical-align: baseline; + span { + padding: 0; + } + } + .hljs-number { + color: #f5871f; } .hljs-string { // For words wrapped in quotation marks - color: #FFF; - } - color: #FFF; - background-color: #AE6DDF; - border-radius: 3px; - white-space: pre; - vertical-align: baseline; - span { - padding: 0; - } - } - .hljs-number { - color: #f5871f; - } - .hljs-string { // For words wrapped in quotation marks - color: #4fd061; - .hljs-keyword { color: #4fd061; + .hljs-keyword { + color: #4fd061; + } } + background-color: @ui-off-white; + border: none; + padding: 0; } - background-color: @ui-off-white; - border: none; - padding: 0; } - } + } @media (max-width: 991px) { [purpose='page-container'] { padding: 32px; diff --git a/website/assets/styles/pages/vital-details.less b/website/assets/styles/pages/vital-details.less index 0fce8d0c51..a61cb26b1e 100644 --- a/website/assets/styles/pages/vital-details.less +++ b/website/assets/styles/pages/vital-details.less @@ -309,26 +309,83 @@ [purpose='vital-check'] { padding-bottom: 16px; [purpose='codeblock'] { + margin-top: 24px; + [purpose='codeblock-tabs'] { + background: var(--UI-Fleet-Black-5, #F4F4F6); + border-top: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-radius: 4px 4px 0px 0px; + height: 35px; + display: flex; + flex-direction: row; + align-items: flex-end; + } + [purpose='codeblock-tab'] { + border-bottom: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + cursor: pointer; + height: 35px; + padding: 8px 24px; + color: #000; + display: flex; + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 18px; /* 150% */ + [purpose='new-badge'] { + color: #FFF; + font-family: 'Source Code Pro'; + font-size: 10px; + font-style: normal; + font-weight: 600; + line-height: 11px; /* 110% */ + display: inline-flex; + padding: 2px 8px; + justify-content: center; + align-items: center; + margin-left: 10px; + border-radius: var(--spacing-half, 4px); + border: 1px solid var(--UI-Fleet-Black-50, #8B8FA2); + background: var(--UI-Fleet-Black-50, #8B8FA2); + height: 19px; + } + &.selected { + [purpose='new-badge'] { + height: 19px; + border-radius: var(--spacing-half, 4px); + border: 1px solid var(--color-brand-blue, #0587FF); + background: var(--color-brand-blue, #0587FF); + } + background: #F9FAFC; + border-radius: 4px 4px 0px 0px; + border-top: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-right: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-left: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-bottom: 1px solid var(--UI-Fleet-Black-10, #F9FAFC); + } + } padding: 0; position: relative; + [purpose='copy-button-tab'] { + width: 100%; + height: 35px; + border-radius: 4px 4px 0px 0px; + border-right: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-bottom: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + } [purpose='copy-button'] { position: absolute; - top: 11px; + top: 2px; right: 10px; border-radius: 8px; height: 32px; width: 32px; background: url('/images/icon-copy-16x16@2x.png'); - background-color: #F9FAFC; background-size: 14px 14px; background-position: center; background-repeat: no-repeat; cursor: pointer; - &:hover { - background-color: #F2F2F5; - } &.copied { - background: url('/images/icon-copy-clicked-checkmark-32x32@2x.png'); + background: url('/images/icon-copy-clicked-checkmark-no-background-32x32@2x.png'); background-size: 32px 32px; background-repeat: no-repeat; background-position: center; @@ -340,10 +397,11 @@ width: 100%; max-width: 100%; padding: 16px 44px 16px 24px; - border: 1px solid #E2E4EA; + border-right: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-left: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); + border-bottom: 1px solid var(--UI-Fleet-Black-10, #E2E4EA); background: #F9FAFC; - border-radius: 4px; - margin-top: 16px; + border-radius: 0px 0px 4px 4px; code { color: #515774; &.has-linebreaks { diff --git a/website/scripts/get-powershell-commands-and-regenerate-queries-yaml.js b/website/scripts/get-powershell-commands-and-regenerate-queries-yaml.js new file mode 100644 index 0000000000..6912e26bf8 --- /dev/null +++ b/website/scripts/get-powershell-commands-and-regenerate-queries-yaml.js @@ -0,0 +1,59 @@ +module.exports = { + + + friendlyName: 'Get Powershell commands and regenerate queries yaml', + + + description: '', + + + + fn: async function () { + let path = require('path'); + let YAML = require('yaml'); + + let topLvlRepoPath = path.resolve(sails.config.appPath, '../'); + + let RELATIVE_PATH_TO_QUERY_LIBRARY_YML_IN_FLEET_REPO = 'docs/01-Using-Fleet/standard-query-library/standard-query-library.yml'; + let newYaml = ''; + + let yaml = await sails.helpers.fs.read(path.join(topLvlRepoPath, RELATIVE_PATH_TO_QUERY_LIBRARY_YML_IN_FLEET_REPO)).intercept('doesNotExist', (err)=>new Error(`Could not find standard query library YAML file at "${RELATIVE_PATH_TO_QUERY_LIBRARY_YML_IN_FLEET_REPO}". Was it accidentally moved? Raw error: `+err.message)); + + let queries = YAML.parseAllDocuments(yaml).map((yamlDocument)=>{ + let query = yamlDocument.toJSON(); + return query; + }); + let batchesOfQueries = _.chunk(queries, 5); + for(let batch of batchesOfQueries) { + await sails.helpers.flow.simultaneouslyForEach(batch, async (query)=>{ + if(query.kind === 'query'){ + return; + } + if(!query.spec.platform.includes('windows')) { + newYaml += '---\n'+YAML.stringify(query); + return; + } + if(query.powershell){ + return; + } + let prompt = ` + Please convert this osquery SQL to a powershell script that writes a comparable result to stdout and does not use osqueryi + \`\`\` + ${query.spec.query} + \`\`\` + + Please return only the powershell script. do not wrap it in any code fences or format it in any way or add any other text. + `; + // console.log(prompt); + let powershellResult = await sails.helpers.ai.prompt.with({prompt:prompt, baseModel: 'o3-mini-2025-01-31'}); + query.spec.powershell = powershellResult; + }); + } + + // TODO: this regenerates the queries.yml file but does not keep the order. + await sails.helpers.fs.write(path.join(topLvlRepoPath, 'docs/new.queries.yml'), newYaml, true); + + } + + +}; diff --git a/website/views/pages/policy-details.ejs b/website/views/pages/policy-details.ejs index 2d3551d22a..f29b1a0e13 100644 --- a/website/views/pages/policy-details.ejs +++ b/website/views/pages/policy-details.ejs @@ -58,8 +58,15 @@

Check

Use the policy below to verify

-
-
<%- policy.query %>
+
+ Query + PowerShellNEW +
+
+
+
+
<%= policy.query %>
+
<%= policy.powershell %>
@@ -74,14 +81,6 @@ ChromeOS -
-

Share

-
- Share this article on Hacker News - Share this article on LinkedIn - Share this article on Twitter -
-
Docs REST API diff --git a/website/views/pages/query-detail.ejs b/website/views/pages/query-detail.ejs index 40f7a2e1a9..ae61a3d26c 100644 --- a/website/views/pages/query-detail.ejs +++ b/website/views/pages/query-detail.ejs @@ -39,8 +39,15 @@

To learn more about queries, check this guide

-
-
<%- query.query %>
+
+ Query + PowerShellNEW +
+
+
+
+
<%= query.query %>
+
<%= query.powershell %>
@@ -49,23 +56,14 @@

Platform

- macOS - Windows - Linux - ChromeOS -
-
-
-

Share

-
- Share this article on Hacker News - Share this article on LinkedIn - Share this article on Twitter + macOS + Windows + Linux + ChromeOS
- Suggest an editSuggest an edit - Talk to an engineerTalk to us + Suggest an editEdit
diff --git a/website/views/pages/vital-details.ejs b/website/views/pages/vital-details.ejs index 0cda750fa7..61d2a95e64 100644 --- a/website/views/pages/vital-details.ejs +++ b/website/views/pages/vital-details.ejs @@ -91,8 +91,15 @@

<%- thisVital.description %>

-
-
<%- thisVital.query %>
+
+ Query + PowerShellNEW +
+
+
+
+
<%= thisVital.query %>
+
<%= thisVital.powershell %>
<% if(thisVital.discovery) {%>