Documentation Index Fetch the complete documentation index at: https://docs.velatir.com/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Velatir local detection scans managed devices for installed AI applications and reports findings to your Velatir dashboard under App Insights . It runs on Windows, macOS, and Linux, and is designed for scheduled execution via MDM platforms such as Microsoft Intune and Jamf Pro.
Prerequisites
A Velatir project API key (from Dashboard > Settings > API Keys )
An MDM platform (Microsoft Intune, Jamf Pro, or similar)
Python 3 on macOS/Linux devices (pre-installed on macOS and most Linux distributions)
Configuration
The detection script reads its API key from a configuration file at a well-known path:
Platform Config File Path Windows C:\ProgramData\Velatir\detection-config.jsonmacOS / Linux /etc/velatir/detection-config.json
Config file format:
{
"apiKey" : "vltr_your_api_key_here"
}
Deploy the config file once with your API key, then deploy the detection script separately. This keeps the API key in one place and makes rotation straightforward.
The macOS/Linux script also accepts --api-key as a command-line argument, which takes precedence over the config file.
Microsoft Intune (Windows)
Deploy using two Intune Remediation packages: one for the configuration file and one for the scan script.
Step 1: Deploy the Configuration File
Go to Devices > Remediations > Create script package
Name it “Velatir Local Detection - Configuration”
Detection script (Detect-VelatirConfig.ps1):
$ConfigPath = "C:\ProgramData\Velatir\detection-config.json"
if ( -not ( Test-Path $ConfigPath )) {
Write-Output "Config file not found"
exit 1
}
try {
$config = Get-Content $ConfigPath - Raw | ConvertFrom-Json
if ([ string ]::IsNullOrWhiteSpace( $config .apiKey ) -or $config .apiKey -eq "vltr_your_api_key_here" ) {
Write-Output "API key not configured"
exit 1
}
Write-Output "Config file valid"
exit 0
}
catch {
Write-Output "Invalid config file"
exit 1
}
See all 20 lines
Remediation script (Remediate-VelatirConfig.ps1):
# Configuration - UPDATE THIS VALUE
$ApiKey = "vltr_your_api_key_here"
$ConfigDir = "C:\ProgramData\Velatir"
$ConfigPath = " $ConfigDir \detection-config.json"
if ( -not ( Test-Path $ConfigDir )) {
New-Item - Path $ConfigDir - ItemType Directory - Force | Out-Null
}
$config = @ { apiKey = $ApiKey } | ConvertTo-Json
Set-Content - Path $ConfigPath - Value $config - Encoding UTF8
Write-Output "Configuration deployed"
See all 14 lines
Configure the script package:
Run this script using the logged-on credentials: No
Enforce script signature check: No
Run script in 64-bit PowerShell: Yes
Assign to your device groups
Set the schedule (e.g., once per day)
Step 2: Deploy the Detection Script
Go to Devices > Remediations > Create script package
Name it “Velatir Local Detection - Scan”
Detection script (Detect-VelatirScan.ps1):
$TimestampPath = "C:\ProgramData\Velatir\last-scan.txt"
if ( -not ( Test-Path $TimestampPath )) {
Write-Output "No previous scan found"
exit 1
}
try {
$lastScan = [ DateTime ]::Parse(( Get-Content $TimestampPath - Raw).Trim())
$hoursSince = ( Get-Date ).Subtract( $lastScan ).TotalHours
if ( $hoursSince -lt 24 ) {
Write-Output "Last scan $( [ math ]::Round( $hoursSince , 1 ) ) h ago"
exit 0
} else {
Write-Output "Scan overdue ( $( [ math ]::Round( $hoursSince , 1 ) ) h)"
exit 1
}
}
catch {
Write-Output "Invalid timestamp file"
exit 1
}
See all 23 lines
Remediation script (Remediate-VelatirScan.ps1):
# Requires -Version 5.1
$ErrorActionPreference = "Stop"
$ScriptVersion = "1.0.0"
$ConfigPath = "C:\ProgramData\Velatir\detection-config.json"
$TimestampPath = "C:\ProgramData\Velatir\last-scan.txt"
$ApiBaseUrl = "https://api.velatir.com"
# Read configuration
if ( -not ( Test-Path $ConfigPath )) {
Write-Error "Config file not found at $ConfigPath . Deploy the configuration first."
exit 1
}
$config = Get-Content $ConfigPath - Raw | ConvertFrom-Json
$ApiKey = $config .apiKey
if ([ string ]::IsNullOrWhiteSpace( $ApiKey )) {
Write-Error "API key is empty in $ConfigPath "
exit 1
}
function Get-MachineId {
try {
$cs = Get-CimInstance - ClassName Win32_ComputerSystemProduct
return $cs .UUID
}
catch {
$os = Get-CimInstance - ClassName Win32_OperatingSystem
return " $ env: COMPUTERNAME - $( $os .InstallDate.ToString ( 'yyyyMMdd' ) ) "
}
}
function Get-Manifest {
$headers = @ {
"X-API-Key" = $ApiKey
"Accept" = "application/json"
}
return Invoke-RestMethod - Uri " $ApiBaseUrl /api/v1/application-manifest" `
- Headers $headers - Method Get
}
function Test-ProcessRunning {
param ([ string ] $ProcessName )
$proc = Get-Process - Name ( $ProcessName -replace '\.exe$' , '' ) - ErrorAction SilentlyContinue
return ( $null -ne $proc )
}
function Test-PathExists {
param ([ string ] $Path )
$expandedPath = [ Environment ]::ExpandEnvironmentVariables( $Path )
return ( Test-Path $expandedPath )
}
function Test-RegistryKey {
param ([ string ] $RegistryPath )
return ( Test-Path $RegistryPath )
}
function Test-DockerImage {
param ([ string ] $ImageName )
try {
$images = & docker ps -- format "{{.Image}}" -- no - trunc 2> $null
if ( -not $images ) { return $false }
foreach ( $image in $images ) {
if ( $image -like " $ImageName *" ) { return $true }
}
return $false
}
catch { return $false }
}
function Scan-Tool {
param ( $Tool )
$findings = @ ()
$detections = $Tool .detections.windows
if ( -not $detections ) { return $findings }
foreach ( $detection in $detections ) {
$found = $false
switch ( $detection .type ) {
"process" { $found = Test-ProcessRunning - ProcessName $detection .value }
"path" { $found = Test-PathExists - Path $detection .value }
"registry" { $found = Test-RegistryKey - RegistryPath $detection .value }
"docker" { $found = Test-DockerImage - ImageName $detection .value }
}
if ( $found ) {
$findings += @ {
toolId = $Tool .id
toolCategory = $Tool .category
detectionSource = $detection .type
detectedValue = $detection .value
}
}
}
return $findings
}
function Submit-Findings {
param ( $Findings , $MachineId , $Hostname )
$body = @ {
machineId = $MachineId
hostname = $Hostname
osType = "windows"
osVersion = [ System.Environment ]::OSVersion.VersionString
scriptVersion = $ScriptVersion
findings = $Findings
} | ConvertTo-Json - Depth 10
$headers = @ {
"X-API-Key" = $ApiKey
"Content-Type" = "application/json"
"Accept" = "application/json"
}
Invoke-RestMethod - Uri " $ApiBaseUrl /api/v1/application-findings" `
- Headers $headers - Method Post - Body $body | Out-Null
}
# --- Main ---
Write-Host "Velatir Local Detection v $ScriptVersion (Windows)"
Write-Host "Downloading manifest..."
$manifest = Get-Manifest
$toolCount = $manifest .tools.Count
Write-Host "Manifest loaded: $toolCount tools to scan"
$allFindings = @ ()
foreach ( $tool in $manifest .tools ) {
$allFindings += Scan - Tool - Tool $tool
}
$machineId = Get-MachineId
$hostname = $ env: COMPUTERNAME
Write-Host "Scan complete: $( $allFindings .Count ) applications detected"
Write-Host "Submitting findings..."
Submit-Findings - Findings $allFindings - MachineId $machineId - Hostname $hostname
# Record scan timestamp
Set-Content - Path $TimestampPath - Value ( Get-Date - Format "o" ) - Encoding UTF8
Write-Host "Done."
See all 141 lines
Configure the script package:
Run this script using the logged-on credentials: No
Enforce script signature check: No
Run script in 64-bit PowerShell: Yes
Assign to your device groups
Set the schedule (e.g., once per day)
Why two separate Remediation packages? Separating the configuration and scan into two packages allows you to update the API key independently, and ensures the config file is in place before the scan runs. Intune Remediations cannot guarantee execution order within a single package.
Jamf Pro (macOS)
Step 1: Deploy the Configuration File
Create a Jamf Pro policy to deploy the configuration file.
Go to Computers > Policies > New
Name it “Velatir Local Detection - Configuration”
Add a Scripts payload with the following script:
#!/bin/bash
# Configuration - UPDATE THIS VALUE
API_KEY = "vltr_your_api_key_here"
CONFIG_DIR = "/etc/velatir"
CONFIG_PATH = " $CONFIG_DIR /detection-config.json"
mkdir -p " $CONFIG_DIR "
cat > " $CONFIG_PATH " << EOF
{
"apiKey": " $API_KEY "
}
EOF
chmod 644 " $CONFIG_PATH "
echo "Configuration deployed to $CONFIG_PATH "
See all 16 lines
Set Trigger to Enrollment Complete and Recurring Check-in
Set Execution Frequency to Once per computer
Scope to your target computers
Step 2: Deploy the Detection Script
Go to Computers > Policies > New
Name it “Velatir Local Detection - Scan”
Add a Scripts payload with the following script:
#!/usr/bin/env bash
# Velatir Local Detection for macOS/Linux
set -euo pipefail
SCRIPT_VERSION = "1.0.0"
API_BASE_URL = "https://api.velatir.com"
API_KEY = ""
CONFIG_PATH = "/etc/velatir/detection-config.json"
# Parse arguments
while [[ $# -gt 0 ]]; do
case " $1 " in
--api-key )
API_KEY = " $2 "
shift 2
;;
--api-base-url )
API_BASE_URL = " $2 "
shift 2
;;
*)
shift
;;
esac
done
# Read from config file if no CLI argument provided
if [[ -z " $API_KEY " ]] && [[ -f " $CONFIG_PATH " ]]; then
API_KEY = $( python3 -c "import json; print(json.load(open(' $CONFIG_PATH ')).get('apiKey', ''))" 2> /dev/null || true )
fi
if [[ -z " $API_KEY " ]]; then
echo "Error: No API key provided." >&2
echo "Either use --api-key <key> or create a config file at $CONFIG_PATH " >&2
echo "Config format: { \" apiKey \" : \" vltr_your_api_key_here \" }" >&2
exit 1
fi
# Detect OS
detect_os () {
case "$( uname -s )" in
Darwin ) echo "mac" ;;
Linux ) echo "linux" ;;
*) echo "unknown" ;;
esac
}
OS_TYPE = "$( detect_os )"
# Get machine ID
get_machine_id () {
if [[ " $OS_TYPE " == "mac" ]]; then
ioreg -rd1 -c IOPlatformExpertDevice 2> /dev/null \
| awk '/IOPlatformUUID/{gsub(/"/, "", $3); print $3}' \
|| echo "$( hostname )-$( date +%s)"
else
if [[ -f /etc/machine-id ]]; then
cat /etc/machine-id
elif [[ -f /var/lib/dbus/machine-id ]]; then
cat /var/lib/dbus/machine-id
else
echo "$( hostname )-$( date +%s)"
fi
fi
}
get_os_version () {
if [[ " $OS_TYPE " == "mac" ]]; then
sw_vers -productVersion 2> /dev/null || echo "unknown"
else
if [[ -f /etc/os-release ]]; then
. /etc/os-release
echo "${ PRETTY_NAME :- $ID $VERSION_ID }"
else
uname -r
fi
fi
}
MACHINE_ID = "$( get_machine_id )"
HOSTNAME = "$( hostname )"
OS_VERSION = "$( get_os_version )"
echo "Velatir Local Detection v${ SCRIPT_VERSION } (${ OS_TYPE })"
echo "Downloading manifest..."
# Download manifest
MANIFEST = $( curl -sf \
-H "X-API-Key: ${ API_KEY }" \
-H "Accept: application/json" \
"${ API_BASE_URL }/api/v1/application-manifest" )
TOOL_COUNT = $( echo " $MANIFEST " | python3 -c \
"import sys,json; print(len(json.load(sys.stdin).get('tools',[])))" 2> /dev/null || echo "?" )
echo "Manifest loaded: ${ TOOL_COUNT } tools to scan"
# Scan and build findings using Python
FINDINGS = $( python3 -c "
import json, subprocess, os
manifest = json.loads('''${ MANIFEST }''')
os_type = '${ OS_TYPE }'
findings = []
# Build list of real user home directories (important when running as root)
home_base = '/Users' if os_type == 'mac' else '/home'
user_homes = []
if os.path.isdir(home_base):
for entry in os.listdir(home_base):
home = os.path.join(home_base, entry)
if os.path.isdir(home) and entry not in ('Shared', 'Guest'):
user_homes.append(home)
def check_path(path):
if path.startswith('~/') or path == '~':
rel = path[2:] if path.startswith('~/') else ''
for home in user_homes:
if os.path.exists(os.path.join(home, rel)):
return True
# Fallback: also check current user's home
expanded = os.path.expanduser(path)
return os.path.exists(expanded)
else:
return os.path.exists(os.path.expandvars(path))
for tool in manifest.get('tools', []):
detections = tool.get('detections', {}).get(os_type, [])
if not detections:
continue
for detection in detections:
det_type = detection.get('type', '')
det_value = detection.get('value', '')
found = False
try:
if det_type == 'process':
result = subprocess.run(['pgrep', '-x', det_value],
capture_output=True, timeout=5)
found = result.returncode == 0
elif det_type == 'path':
found = check_path(det_value)
elif det_type == 'bundle' and os_type == 'mac':
app_dirs = ['/Applications', '/System/Applications']
for home in user_homes:
app_dirs.append(os.path.join(home, 'Applications'))
for app_dir in app_dirs:
if not os.path.isdir(app_dir):
continue
for entry in os.listdir(app_dir):
plist = os.path.join(app_dir, entry, 'Contents', 'Info.plist')
if os.path.exists(plist):
try:
result = subprocess.run(
['/usr/libexec/PlistBuddy', '-c',
'Print :CFBundleIdentifier', plist],
capture_output=True, text=True, timeout=5)
if result.stdout.strip() == det_value:
found = True
break
except:
pass
if found:
break
elif det_type == 'vscode_extension':
ext_id = det_value.lower()
# Check extensions directories on disk (works when running as root)
ext_dirs = ['.vscode/extensions', '.vscode-insiders/extensions',
'.cursor/extensions', '.void/extensions']
for home in user_homes + [os.path.expanduser('~')]:
for ext_dir in ext_dirs:
full_dir = os.path.join(home, ext_dir)
if not os.path.isdir(full_dir):
continue
for entry in os.listdir(full_dir):
if entry.lower().startswith(ext_id + '-') or entry.lower() == ext_id:
found = True
break
if found:
break
if found:
break
elif det_type == 'docker':
try:
result = subprocess.run(
['docker', 'ps', '--format', '{{.Image}}', '--no-trunc'],
capture_output=True, text=True, timeout=10)
if result.returncode == 0:
for image in result.stdout.strip().split('\n'):
if image.startswith(det_value):
found = True
break
except:
pass
except Exception:
pass
if found:
findings.append({
'toolId': tool['id'],
'toolCategory': tool.get('category'),
'detectionSource': det_type,
'detectedValue': det_value
})
print(json.dumps(findings))
" 2> /dev/null )
FINDING_COUNT = $( echo " $FINDINGS " | python3 -c \
"import sys,json; print(len(json.load(sys.stdin)))" 2> /dev/null || echo "0" )
echo "Scan complete: ${ FINDING_COUNT } applications detected"
echo "Submitting findings..."
# Build and submit payload
PAYLOAD = $( python3 -c "
import json
findings = json.loads('''${ FINDINGS }''')
payload = {
'machineId': '${ MACHINE_ID }',
'hostname': '${ HOSTNAME }',
'osType': '${ OS_TYPE }',
'osVersion': '''${ OS_VERSION }''',
'scriptVersion': '${ SCRIPT_VERSION }',
'findings': findings
}
print(json.dumps(payload))
" )
curl -sf -X POST \
-H "X-API-Key: ${ API_KEY }" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "${ PAYLOAD }" \
"${ API_BASE_URL }/api/v1/application-findings" > /dev/null
echo "Done."
See all 236 lines
Set Trigger to Recurring Check-in
Set Execution Frequency to Once every day
Scope to your target computers
Linux
The same macOS/Linux script works on Linux. Deploy it via your configuration management tool (Ansible, Puppet, Chef, etc.) or schedule it with cron:
1. Deploy the configuration file:
sudo mkdir -p /etc/velatir
echo '{"apiKey": "vltr_your_api_key_here"}' | sudo tee /etc/velatir/detection-config.json
sudo chmod 644 /etc/velatir/detection-config.json
2. Schedule with cron:
# Save the script to /opt/velatir/detect.sh and make it executable
sudo crontab -e
# Add: run daily at 9 AM
0 9 * * * /opt/velatir/detect.sh >> /var/log/velatir-detection.log 2>&1
Verification
After deployment, verify local detection is working:
Windows
Check the config file exists:
Get-Content "C:\ProgramData\Velatir\detection-config.json"
Check for a recent scan timestamp:
Get-Content "C:\ProgramData\Velatir\last-scan.txt"
macOS
Check the config file exists:
cat /etc/velatir/detection-config.json
Dashboard
Go to App Insights in your Velatir dashboard to see detected applications across your fleet.
Troubleshooting
Python not found (macOS/Linux)
Local detection requires Python 3. On macOS it is pre-installed. On Linux, install it via your package manager:
# Debian/Ubuntu
sudo apt-get install python3
# RHEL/CentOS
sudo yum install python3
Network or firewall issues
The detection script needs outbound HTTPS access to api.velatir.com. Verify connectivity:
curl -sf https://api.velatir.com/healthz
Invoke-RestMethod - Uri "https://api.velatir.com/healthz"
Config file permissions
Windows : The config file in C:\ProgramData\Velatir\ is accessible to SYSTEM and administrators by default. Intune Remediations run as SYSTEM.
macOS/Linux : Ensure the config file is readable by the user running the script (chmod 644).
32-bit vs 64-bit context (Windows)
Ensure Run script in 64-bit PowerShell is enabled in your Intune Remediation settings. Running in 32-bit context may cause Get-CimInstance or registry checks to behave differently.
Get API Key Set up your Velatir account and get a project API key
App Insights View detected applications in your dashboard