Skip to content

[PPSC-697] feat: agent discovery#150

Open
dmitrysmirnov-armis wants to merge 8 commits intomainfrom
feat/PPSC-697-agent-discovery
Open

[PPSC-697] feat: agent discovery#150
dmitrysmirnov-armis wants to merge 8 commits intomainfrom
feat/PPSC-697-agent-discovery

Conversation

@dmitrysmirnov-armis
Copy link
Copy Markdown

@dmitrysmirnov-armis dmitrysmirnov-armis commented Apr 28, 2026

Related Issue

Type of Change

  • New feature (non-breaking change which adds functionality)

Problem

Armis Cloud has no visibility into which AI coding agents (Claude Code, Cursor, Copilot, Windsurf, etc.) are installed across developer workstations, nor whether those agents have the Armis AppSec MCP server configured. This data is needed to track agent adoption and MCP deployment coverage.

Solution

Add a new armis-cli agent-detection command that scans the local filesystem for 15 AI coding agents and reports whether each has Armis AppSec MCP installed.

Agent detection (internal/agentdetect/):

  • Detects 15 agents: Claude Code, Windsurf, Google Antigravity, GitHub Copilot, Cursor, Cline, Roo Code, Aider, Devin, OpenHands, Amazon Q, Junie, Zed, Continue, Gemini CLI
  • Detection works by checking for config directories, VS Code extensions, JetBrains plugins, and platform-specific binary paths
  • MCP installation status checked per-agent by parsing each agent's config format (mcp.json, settings.json, mcp_settings.json, context_servers, enabledPlugins)
  • Platform-specific implementations for macOS, Linux, and Windows (user enumeration, app paths, JetBrains dirs)
  • When run as root/admin, scans all user profiles; as regular user, scans only current user

Output formats:

  • --format plain (default): grouped by user, one line per user with agents and MCP status
  • --format json: flat array of DetectedAgent objects

Install command updates:

  • Added Roo Code and Junie as new installable editor targets
  • Added OpenHands to the "manual setup required" list

Testing

Automated Tests

  • Unit tests added/updated
  • Integration tests added/updated
  • All tests passing locally

Manual Testing

  1. armis-cli agent-detection — verified detection of locally installed agents (Claude Code, Cursor) with correct MCP status
  2. sudo armis-cli agent-detection --format json — scan all users on the machine, JSON output
  3. armis-cli install roocode / armis-cli install junie — verified new editor targets work

Reviewer Notes


Checklist

  • Code follows project style guidelines
  • Pre-commit hooks pass
  • Self-review performed
  • Documentation updated (if needed)
  • Breaking changes documented (if applicable)
  • No new warnings generated

Comment thread internal/agentdetect/agentdetect.go Fixed
Comment thread internal/agentdetect/agentdetect.go Fixed
Comment thread internal/agentdetect/detector.go Fixed
Comment thread internal/agentdetect/detector.go Fixed
Comment thread internal/agentdetect/detector.go Fixed
Comment thread internal/agentdetect/mcpconfig.go Fixed
Comment thread internal/agentdetect/platform_linux.go Fixed
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Armis AppSecArmis AppSec Security Scan Results

🟠 HIGH issues found

Severity Count
🟠 HIGH 5
🟡 MEDIUM 5

Total: 10

View all 10 findings

🟠 HIGH (5)

CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') - Broken Access Control (CWE-22

Location: internal/agentdetect/platform_linux.go:29

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')): The function JetBrainsPluginDirs builds a file path by taking the homeDir argument and appending a fixed sub‑directory (.local/share/JetBrains). It does this with filepath.Join and then passes the result to globJetBrainsPluginDirs. Because the code does not check or clean the homeDir value, an attacker who can control that argument could include “..” components (for example, ../../etc) to move the path outside the intended user home directory. The resulting path would be used by the glob function, which could cause the program to read files or directories that it should not access. In this snippet there is no special validation or sanitising step before the path is built, so the risky operation can be reached directly from the supplied homeDir. The function itself is not exposed as a network service or public API, so the issue would only be exploitable if some part of the program that receives external input eventually calls this function with that input. To protect against this, the code should verify that homeDir is a legitimate absolute path that stays inside the expected base directory, or reject any path components that try to traverse upward.

CWEs: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') - Broken Access Control (CWE-22

Location: internal/agentdetect/platform_linux.go:25

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')): The function VSCodeExtensionsDir builds a file system path by simply joining the homeDir argument with the fixed sub‑directories “.vscode/extensions”. It does not check whether the homeDir value is safe or whether it points outside the intended user directory. Because the function directly uses whatever string is passed in, an attacker who can influence the homeDir argument could cause the program to look for files in arbitrary locations, potentially accessing or modifying data it should not. No validation or sanitising steps are present before the path is created, so the risky operation is exposed to any uncontrolled input that reaches this function. This matches the description of a path‑traversal weakness (CWE‑22). The code itself does not show any external interface (like a web server) that would let a remote user provide homeDir, so the issue is not directly reachable from outside the program, but the flaw exists wherever the function is used. Adding checks to ensure homeDir is a legitimate directory (for example, confirming it is under a known base path) would mitigate the problem.

CWEs: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition - The `CheckMCP` function first makes sure that the directory `mcpDir` (the “.continue/mcpServers” folder) is inside the trusted home directory

Location: internal/agentdetect/detector.go:438

The CheckMCP function first makes sure that the directory mcpDir (the “.continue/mcpServers” folder) is inside the trusted home directory. It does this check with isUnderDir. After the check, the code reads the list of files in that folder and, for each file, calls HasArmisMCP on the full path. The problem is that the check only verifies the directory itself, not the individual files that are later processed. An attacker who can create or modify files in the “mcpServers” folder could add a file whose name includes path‑traversal components (for example ../../etc/passwd). When the loop reaches that file, HasArmisMCP will be called with the unvalidated path, allowing the program to read or act on a file outside the trusted home directory. This situation matches a Time‑of‑Check‑Time‑of‑Use (TOCTOU) race condition: the safety check is performed before the directory contents are examined, and the contents can change in the meantime. The code runs as part of a local detection routine; it is not exposed through a network service or public API. An attacker would need the ability to write files in the user’s home directory to exploit the issue. No additional validation or sanitisation of the file names is performed, so the risky operation can be reached directly from attacker‑controlled input.

CWEs: CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition

CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition - The function `dirExists` first checks that the given path is inside a trusted directory by calling `isUnderDir`

Location: internal/agentdetect/detector.go:48

The function dirExists first checks that the given path is inside a trusted directory by calling isUnderDir. After that check it calls os.Stat on the original path. Because the check and the use happen in two separate steps, an attacker who can change the filesystem (for example, replace a symlink) between those two calls could make the check succeed but cause os.Stat to look at a different location. This race condition lets the attacker bypass the directory restriction. The path that reaches dirExists comes from values like homeDir, which can be influenced by the user, and there is no additional validation before the os.Stat call. Therefore the risky operation can be triggered by user‑controlled input, and the code does not protect against the time‑of‑check‑time‑of‑use problem.

CWEs: CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition

CWE-770: Allocation of Resources Without Limits or Throttling - The function `readJSONFileAsMap` reads an entire file into memory using `os.ReadFile` and does not check how big the file is

Location: internal/install/editors.go:310

The function readJSONFileAsMap reads an entire file into memory using os.ReadFile and does not check how big the file is. The name of the file it reads comes from other parts of the program that build the path based on environment variables such as XDG_CONFIG_HOME or APPDATA. If an attacker can influence those environment variables (for example, by running the program with a custom environment), they can cause the program to open a very large file. Because there is no limit on the size, the program may allocate a huge amount of memory, which can lead to a crash or a denial‑of‑service condition. No part of the code limits the size or validates the file before reading it, so the risky operation is reachable from external input.

CWEs: CWE-770: Allocation of Resources Without Limits or Throttling

🟡 MEDIUM (5)

CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition - The `fileExists` function first checks that the given path is inside the allowed home directory by calling `isUnderDir`

Location: internal/agentdetect/detector.go:57

The fileExists function first checks that the given path is inside the allowed home directory by calling isUnderDir. Right after that check it calls os.Stat to look at the file. Between the check and the Stat call an attacker could change what the path points to (for example by replacing a file with a symbolic link). Because the function does not verify the path again after the Stat, it may report that a file exists (or does not exist) based on the attacker‑controlled location. This race condition can be triggered whenever the path argument comes from outside the program, such as a value supplied by a user or another part of the system. No extra validation or re‑checking is performed, so the risky operation can be abused to make the program trust the wrong file.

CWEs: CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition

CWE-770: Allocation of Resources Without Limits or Throttling - The function builds a file‑search pattern and then asks the operating system to find every directory that matches it

Location: internal/agentdetect/userprofile.go:55

The function builds a file‑search pattern and then asks the operating system to find every directory that matches it. The list of matches is stored in a slice and returned without any check on how many items were found. If the pattern matches a very large number of directories, the program could allocate a lot of memory just to hold the list, which can cause the program to run out of memory or become very slow. In this code the pattern is created from the baseDir argument, so the amount of memory used depends on what path is supplied. The function itself is not exposed directly to users over a network, but it could be called by other parts of the program that read configuration values or other internal data. Because the code does not limit the number of matches, an attacker who can influence the baseDir value (for example, by providing a specially crafted configuration) could cause the program to allocate excessive memory. No special checks or limits are present before the matches are stored, so the risky operation is the unbounded allocation of the slice that holds all the matching paths. Adding a limit on how many paths are collected, or validating the input directory, would prevent this problem.

CWEs: CWE-770: Allocation of Resources Without Limits or Throttling

CWE-770: Allocation of Resources Without Limits or Throttling - The function `enumerateUserDirs` reads every entry in a given directory and adds each one to a slice called `users`

Location: internal/agentdetect/userprofile.go:30

The function enumerateUserDirs reads every entry in a given directory and adds each one to a slice called users. It does this without checking how many entries there might be. If someone can choose a directory that contains a huge number of sub‑folders, the slice will keep growing and the program could use a lot of memory, possibly leading to a crash or slowdown. The code itself does not limit the number of items it stores, which matches the description of CWE‑770. Because the function does not receive any special protection or limit, the risky operation (the append on line 30) can consume as much memory as the directory provides. No special checks or safeguards are present to stop this from happening.

CWEs: CWE-770: Allocation of Resources Without Limits or Throttling

CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') - Broken Access Control (CWE-22

Location: internal/agentdetect/mcpconfig.go:23

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')): The function tries to read a file whose location is given by the configPath argument. Before reading, it calls a helper called isUnderDir to check that the path stays inside a specific directory (resolvedHome). The code assumes this check is enough, but we don't see how isUnderDir works. If that helper does not correctly block tricks like “../” sequences, an attacker who can influence configPath could cause the program to read any file on the system that the process can access. This is the classic “path traversal” problem. Because the function directly opens the file with os.ReadFile, the risky operation (reading a file) will happen as soon as a malicious path reaches it. No additional validation or sanitizing steps are shown, so the vulnerable code can be reached. The function itself is not a public web endpoint, but it could be called by other parts of the program that receive data from elsewhere. That means the issue is not directly exposed to the internet, but it could still be exploited if an attacker can influence the configPath argument through any internal call chain. To fix the problem, make sure the path check correctly normalizes the input and rejects any attempt to escape the intended directory, or use a safe library function that enforces the restriction.

CWEs: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

CWE-253: Incorrect Check of Function Return Value - The code tries to look up an editor by its ID with `install.EditorByID(id)`

Location: internal/cmd/install.go:191

The code tries to look up an editor by its ID with install.EditorByID(id). This function returns both the editor object and an error value. At line 191 the error is ignored (e, _ := install.EditorByID(id)), so if the lookup ever failed, the variable e could be nil. Later the code calls e.Register(...) and accesses e.Name, which would cause a crash if e were nil. Because the command‑line arguments are supplied by the user, this part of the program is reachable from a user‑facing interface. An attacker could try to trigger the situation by providing an editor name that the program does not recognize. However, the code already checks the editor’s existence earlier (lines 170‑174) and only adds known IDs to the list, so the nil‑pointer path is unlikely to be reached in practice. Even though the crash may be hard to provoke, the code still ignores the error return, which is exactly what CWE‑253 describes. Adding a proper check for the error (or removing the second call to EditorByID) would fix the issue.

CWEs: CWE-253: Incorrect Check of Function Return Value

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Test Coverage Report

total: (statements) 77.2%

Coverage by function
github.com/ArmisSecurity/armis-cli/cmd/armis-cli/main.go:19:			main					0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/agent.go:34:		Registry				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/agentdetect.go:29:	FlatResults				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/agentdetect.go:44:	NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/agentdetect.go:52:	Scan					83.3%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:12:		resolvePath				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:23:		isUnderDir				81.8%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:44:		dirExists				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:53:		fileExists				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:63:		hasExtensionPrefix			80.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:82:		findExtensionVersion			64.3%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:104:	readVersionFromPackageJSON		71.4%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:119:	hasJetBrainsPlugin			100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:132:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:134:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:139:	CheckMCP				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:143:	DetectVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:151:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:153:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:163:	CheckMCP				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:167:	DetectVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:175:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:177:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:181:	CheckMCP				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:185:	DetectVersion				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:193:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:195:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:205:	CheckMCP				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:210:	DetectVersion				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:218:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:220:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:227:	CheckMCP				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:231:	DetectVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:239:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:241:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:248:	CheckMCP				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:254:	DetectVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:262:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:264:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:271:	CheckMCP				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:275:	DetectVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:283:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:285:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:292:	CheckMCP				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:296:	DetectVersion				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:304:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:306:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:310:	CheckMCP				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:314:	DetectVersion				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:322:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:324:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:328:	CheckMCP				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:332:	DetectVersion				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:340:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:342:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:352:	CheckMCP				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:356:	DetectVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:364:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:366:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:381:	CheckMCP				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:385:	DetectVersion				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:393:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:395:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:403:	CheckMCP				75.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:411:	DetectVersion				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:419:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:421:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:425:	CheckMCP				83.3%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:445:	DetectVersion				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:453:	Name					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:455:	Detect					100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:459:	CheckMCP				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/detector.go:463:	DetectVersion				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/format.go:13:		FormatPlain				82.4%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/format.go:42:		FormatJSON				100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/mcpconfig.go:19:	HasArmisMCP				83.3%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/mcpconfig.go:39:	HasArmisMCPInClaudeSettings		86.7%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/mcpconfig.go:66:	HasArmisMCPInZedSettings		66.7%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/mcpconfig.go:95:	HasArmisMCPInVSCodeFormat		66.7%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/mcpconfig.go:118:	hasArmisMCPInData			100.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/platform_linux.go:13:	NewPlatform				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/platform_linux.go:17:	UserHomeDirs				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/platform_linux.go:24:	VSCodeExtensionsDir			0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/platform_linux.go:28:	JetBrainsPluginDirs			0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/platform_linux.go:32:	VSCodeUserConfigDir			0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/platform_linux.go:36:	CursorAppExists				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/platform_linux.go:40:	JunieBinaryPaths			0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/platform_linux.go:47:	ZedConfigDir				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/platform_linux.go:51:	IsRoot					0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/userprofile.go:12:	enumerateUserDirs			0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/userprofile.go:39:	currentUserOnly				0.0%
github.com/ArmisSecurity/armis-cli/internal/agentdetect/userprofile.go:53:	globJetBrainsPluginDirs			0.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:29:			Error					0.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:72:			copyWithContext				70.4%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:145:			WithHTTPClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:154:			WithUploadHTTPClient			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:162:			WithAllowLocalURLs			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:174:			NewClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:222:			IsDebug					100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:236:			setAuthHeader				77.8%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:270:			StartIngest				72.3%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:429:			GetIngestStatus				82.6%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:470:			WaitForIngest				84.6%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:521:			FetchNormalizedResults			74.2%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:576:			FetchAllNormalizedResults		91.7%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:601:			GetScanResult				68.4%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:636:			WaitForScan				90.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:657:			formatBytes				100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:679:			FetchArtifactScanResults		75.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:734:			ValidatePresignedURL			100.0%
github.com/ArmisSecurity/armis-cli/internal/api/client.go:770:			DownloadFromPresignedURL		84.2%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:58:			NewAuthProvider				95.2%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:104:			GetAuthorizationHeader			100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:124:			GetTenantID				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:141:			GetRegion				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:156:			IsLegacy				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:169:			GetRawToken				85.7%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:196:			exchangeCredentials			87.9%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:267:			refreshIfNeeded				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/auth.go:297:			parseJWTClaims				93.3%
github.com/ArmisSecurity/armis-cli/internal/auth/client.go:29:			Error					100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/client.go:41:			NewAuthClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/client.go:97:			Authenticate				77.4%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:34:		NewRegionCache				100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:40:		Load					82.4%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:74:		Save					76.9%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:104:		Clear					75.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:114:		getFilePath				83.3%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:131:		loadCachedRegion			100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:135:		saveCachedRegion			100.0%
github.com/ArmisSecurity/armis-cli/internal/auth/region_cache.go:139:		clearCachedRegion			100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:60:			InitColors				85.2%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:107:			ColorsEnabled				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:113:			ColorsForced				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:119:			SetOutputToFile				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:125:			GetOutputToFile				0.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:129:			enableColors				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:136:			disableColors				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:151:			parseErrorMessage			92.9%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:182:			PrintError				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:195:			PrintErrorf				0.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:201:			PrintWarning				100.0%
github.com/ArmisSecurity/armis-cli/internal/cli/color.go:206:			PrintWarningf				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/agent_detection.go:36:		init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/agent_detection.go:41:		runAgentDetection			0.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/auth.go:33:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/auth.go:39:			runAuth					92.9%
github.com/ArmisSecurity/armis-cli/internal/cmd/context.go:24:			NewSignalContext			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/context.go:33:			handleScanError				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/help.go:30:			SetupHelp				91.7%
github.com/ArmisSecurity/armis-cli/internal/cmd/help.go:58:			styledUsageTemplate			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/help.go:101:			defaultUsageTemplate			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/help.go:108:			initColorsForHelp			35.3%
github.com/ArmisSecurity/armis-cli/internal/cmd/help.go:149:			styleHelpOutput				83.3%
github.com/ArmisSecurity/armis-cli/internal/cmd/install.go:54:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/install.go:59:			runInstall				0.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/install.go:76:			showInstalledVersions			0.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/install.go:96:			installAll				0.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/install.go:144:			installTargets				0.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/install.go:225:			printCredentialStatus			0.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/output_helper.go:27:		Cleanup					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/output_helper.go:53:		ResolveOutput				96.4%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:149:			SetVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:157:			Execute					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:161:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:192:			PrintUpdateNotification			81.2%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:234:			printUpdateNotificationOnce		75.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:247:			getEnvOrDefault				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:254:			getEnvOrDefaultInt			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:264:			getAPIBaseURL				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:277:			getAuthProvider				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:289:			getPageLimit				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:296:			validatePageLimit			100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:306:			validateFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/root.go:324:			getFailOn				100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan.go:92:			init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan_image.go:152:		init					100.0%
github.com/ArmisSecurity/armis-cli/internal/cmd/scan_repo.go:188:		init					100.0%
github.com/ArmisSecurity/armis-cli/internal/httpclient/client.go:31:		NewClient				100.0%
github.com/ArmisSecurity/armis-cli/internal/httpclient/client.go:57:		Do					86.1%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:23:		NewClaudeInstaller			100.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:32:		InstalledVersion			100.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:37:		Install					15.4%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:65:		pluginCacheDir				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:70:		EnvFilePath				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:75:		GetInstalledVersion			76.2%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:106:		HasExistingEnv				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:111:		registerMarketplace			83.3%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:127:		registerPlugin				75.0%
github.com/ArmisSecurity/armis-cli/internal/install/claude.go:155:		enablePlugin				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:52:		EditorByID				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:65:		ConfigPath				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:76:		IsDetected				80.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:86:		Register				75.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:95:		DetectedEditors				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:112:		NewEditorInstaller			100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:121:		InstalledVersion			100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:124:		PluginDir				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:127:		EnvFilePath				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:130:		HasExistingEnv				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:137:		FetchPlugin				0.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:148:		GetInstalledVersion			80.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:158:		RegisterJetBrains			100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:164:		defaultConfigPath			86.7%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:196:		homeDir					75.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:204:		appSupportPath				29.4%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:235:		registerEditor				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:249:		registerMCPServersFormat		100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:263:		registerVSCodeFormat			100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:282:		registerZedFormat			100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:301:		stdServerEntry				100.0%
github.com/ArmisSecurity/armis-cli/internal/install/editors.go:308:		readJSONFileAsMap			100.0%
github.com/ArmisSecurity/armis-cli/internal/install/plugin.go:47:		newPluginInstaller			100.0%
github.com/ArmisSecurity/armis-cli/internal/install/plugin.go:55:		InstalledVersion			100.0%
github.com/ArmisSecurity/armis-cli/internal/install/plugin.go:60:		FetchAndInstall				0.0%
github.com/ArmisSecurity/armis-cli/internal/install/plugin.go:86:		fetchLatestRelease			69.6%
github.com/ArmisSecurity/armis-cli/internal/install/plugin.go:126:		downloadAndExtract			73.6%
github.com/ArmisSecurity/armis-cli/internal/install/plugin.go:242:		createVenv				0.0%
github.com/ArmisSecurity/armis-cli/internal/install/plugin.go:271:		validateGitHubURL			100.0%
github.com/ArmisSecurity/armis-cli/internal/install/plugin.go:285:		extractFile				57.1%
github.com/ArmisSecurity/armis-cli/internal/install/plugin.go:297:		writeJSON				66.7%
github.com/ArmisSecurity/armis-cli/internal/install/plugin.go:308:		findPython				69.2%
github.com/ArmisSecurity/armis-cli/internal/install/plugin.go:332:		writeEnvFromEnvironment			83.3%
github.com/ArmisSecurity/armis-cli/internal/install/plugin.go:355:		writeHelperScript			0.0%
github.com/ArmisSecurity/armis-cli/internal/install/plugin.go:401:		venvPython				66.7%
github.com/ArmisSecurity/armis-cli/internal/output/errno_unix.go:12:		isSyncNotSupported			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:54:			wrapText				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:77:			wrapLine				91.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:115:		formatRecommendations			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:185:		wrapTextWithFirstLinePrefix		90.9%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:224:		write					66.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:255:		Write					89.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:285:		Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:290:		FormatWithOptions			84.4%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:360:		SyncColors				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:364:		sortFindingsBySeverity			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:375:		loadSnippetFromFile			69.4%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:487:		formatCodeSnippetWithFrame		91.1%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:580:		truncatePlainLine			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:592:		highlightColumns			93.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:637:		scanDuration				89.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:670:		pluralize				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:679:		renderBriefStatus			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:719:		renderSummaryDashboard			56.4%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:800:		renderFindings				88.9%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:829:		renderFinding				69.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:919:		renderGroupedFindings			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:943:		groupFindings				96.8%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1000:		severityRank				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1007:		isGitRepo				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1014:		getGitBlame				38.1%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1051:		parseGitBlame				95.2%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1087:		maskEmail				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1110:		getTopLevelDomain			75.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1122:		getHumanDisplayTitle			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1136:		wrapTitle				93.9%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1195:		maskFixForDisplay			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1230:		formatFixSection			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1295:		formatProposedSnippet			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1378:		limitHunkContext			64.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1454:		parseDiffHunk				91.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1476:		parseDiffLines				94.6%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1567:		findInlineChanges			73.5%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1638:		computeLCS				92.3%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1690:		buildTokenPositions			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1706:		tokenizeLine				92.9%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1734:		isWordChar				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1741:		formatDiffWithColorsStyled		77.1%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1815:		extractDiffFilename			80.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1837:		formatDiffHunkLine			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1857:		formatDiffContextLine			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1868:		formatDiffRemoveLine			86.4%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1909:		formatDiffAddLine			86.4%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1951:		applyInlineHighlights			81.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:1993:		truncateDiffLine			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2000:		truncateDiffLineWithFlag		66.7%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2014:		adjustHighlightSpans			83.3%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2036:		groupDiffHunks				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2067:		collectRenderOps			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2110:		renderChangeBlock			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2169:		formatDiffHunkSeparator			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2184:		formatValidationSection			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/human.go:2241:		getExposureDescription			0.0%
github.com/ArmisSecurity/armis-cli/internal/output/icons.go:24:			GetConfidenceIcon			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:15:			Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:24:			FormatWithOptions			66.7%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:32:			formatWithDebug				0.0%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:58:			maskScanResultForOutput			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/json.go:78:			maskFindingSecrets			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:48:			Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:55:			FormatWithOptions			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:63:			formatWithSeverities			83.3%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:88:			isFailureSeverity			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:98:			convertToJUnitCasesWithSeverities	91.7%
github.com/ArmisSecurity/armis-cli/internal/output/junit.go:130:		countFailuresWithSeverities		100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:24:		Error					0.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:35:		Error					0.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:55:		GetFormatter				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:71:		ShouldFail				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/output.go:89:		CheckExit				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:164:		stripMarkdown				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:175:		Format					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:202:		firstNonEmpty				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:218:		stableRuleID				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:233:		computeFingerprint			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:254:		buildRules				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:319:		convertToSarifResults			89.7%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:420:		buildMessageText			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:427:		severityToSarifLevel			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:446:		severityToSecurityScore			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:465:		generateHelpURI				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:488:		convertFixToSarif			90.5%
github.com/ArmisSecurity/armis-cli/internal/output/sarif.go:605:		FormatWithOptions			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/styles.go:138:		DefaultStyles				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/styles.go:276:		NoColorStyles				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/styles.go:353:		GetStyles				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/styles.go:361:		SyncStylesWithColorMode			100.0%
github.com/ArmisSecurity/armis-cli/internal/output/styles.go:386:		GetSeverityText				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/styles.go:414:		TerminalWidth				33.3%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:21:		GetLexer				100.0%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:32:		GetChromaStyle				80.0%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:45:		HighlightCode				81.2%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:79:		HighlightLine				75.0%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:88:		getTerminalFormatter			60.0%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:103:		HighlightLineWithBackground		87.5%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:126:		getBackgroundANSI			58.3%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:158:		rgbToANSI256				0.0%
github.com/ArmisSecurity/armis-cli/internal/output/syntax.go:171:		parseHexColor				76.9%
github.com/ArmisSecurity/armis-cli/internal/output/writer.go:51:		validateOutputPath			92.3%
github.com/ArmisSecurity/armis-cli/internal/output/writer.go:88:		NewFileOutput				88.2%
github.com/ArmisSecurity/armis-cli/internal/output/writer.go:142:		Writer					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/writer.go:147:		Close					100.0%
github.com/ArmisSecurity/armis-cli/internal/output/writer.go:164:		FormatFromExtension			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:32:		IsCI					100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:60:		isTerminalWriter			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:68:		NewReader				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:83:		NewWriter				50.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:117:		NewSpinner				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:125:		NewSpinnerWithTimeout			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:141:		NewSpinnerWithContext			100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:149:		SetWriter				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:158:		Start					86.4%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:268:		Stop					100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:303:		Update					100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:310:		GetElapsed				100.0%
github.com/ArmisSecurity/armis-cli/internal/progress/progress.go:317:		formatDuration				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/finding_type.go:9:		DeriveFindingType			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:48:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:63:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:69:		WithFetchRetryInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:75:		WithSBOMVEXOptions			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:82:		WithPullPolicy				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:88:		ScanImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:119:		ScanTarball				77.8%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:230:		exportImage				0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:280:		isDockerAvailable			42.9%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:294:		getDockerCommand			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:303:		validateDockerCommand			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:311:		imageExistsLocally			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:319:		determinePullBehavior			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:337:		isRetryableError			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:345:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:372:		convertNormalizedFindings		85.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:495:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:514:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:533:		isEmptyFinding				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/image.go:548:		generateFindingTitle			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/image/validate.go:11:		validateImageName			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/mask.go:21:			MaskFixSecrets				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/files.go:26:		ParseFileList				87.5%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/files.go:41:		addFile					87.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/files.go:93:		Files					100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/files.go:98:		RepoRoot				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/files.go:103:		ValidateExistence			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:52:		GitChangedFiles				82.6%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:102:	gitRepoRoot				80.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:126:	changedUncommitted			41.7%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:155:	changedStaged				75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:168:	validateRef				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:181:	changedSinceRef				75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:204:	filterToScanPath			94.1%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:239:	runGit					91.7%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:265:	parseLines				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/gitchanges.go:285:	combineAndDedupe			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:23:		LoadIgnorePatterns			88.9%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:60:		loadIgnoreFile				88.5%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:105:		Match					100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/ignore.go:117:		shouldSkipDir				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:46:		NewScanner				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:61:		WithPollInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:67:		WithFetchRetryInterval			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:73:		WithIncludeFiles			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:79:		WithSBOMVEXOptions			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:85:		Scan					72.7%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:267:		tarGzDirectory				71.8%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:350:		isPathContained				75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:359:		tarGzFiles				78.6%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:450:		safeAddSize				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:457:		calculateFilesSize			78.6%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:482:		calculateDirSize			79.2%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:526:		shouldSkip				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:557:		isTestFile				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:603:		isRetryableError			75.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:612:		buildScanResult				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:639:		convertNormalizedFindings		73.3%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:762:		shouldFilterByExploitability		100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:781:		cleanDescription			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:802:		generateFindingTitle			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/repo/repo.go:806:		isEmptyFinding				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/sbom_vex.go:38:		NewSBOMVEXDownloader			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/sbom_vex.go:50:		Download				85.2%
github.com/ArmisSecurity/armis-cli/internal/scan/sbom_vex.go:102:		downloadAndSave				77.8%
github.com/ArmisSecurity/armis-cli/internal/scan/status.go:16:			FormatScanStatus			100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/status.go:35:			FormatElapsed				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/status.go:48:			MapSeverity				100.0%
github.com/ArmisSecurity/armis-cli/internal/scan/testhelpers/findings.go:9:	CreateNormalizedFinding			0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/testhelpers/findings.go:14:	CreateNormalizedFindingWithLabels	0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/testhelpers/findings.go:19:	CreateNormalizedFindingFull		0.0%
github.com/ArmisSecurity/armis-cli/internal/scan/title.go:14:			GenerateFindingTitle			0.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:63:		NewChecker				100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:79:		CheckCached				100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:97:		CheckInBackground			100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:117:		check					85.7%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:160:		fetchLatestVersion			89.5%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:193:		getCacheFilePath			66.7%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:208:		readCache				84.6%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:231:		writeCache				76.9%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:254:		IsNewer					100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:277:		parseVersion				100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:300:		FormatNotification			100.0%
github.com/ArmisSecurity/armis-cli/internal/update/update.go:319:		getUpdateCommand			40.0%
github.com/ArmisSecurity/armis-cli/internal/util/cache.go:21:			GetCacheDir				75.0%
github.com/ArmisSecurity/armis-cli/internal/util/cache.go:41:			GetCacheFilePath			80.0%
github.com/ArmisSecurity/armis-cli/internal/util/format.go:7:			FormatCategory				100.0%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:109:			MaskSecretInLine			86.4%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:163:			maskValue				83.3%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:189:			MaskSecretInLines			100.0%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:203:			MaskSecretInMultiLineString		100.0%
github.com/ArmisSecurity/armis-cli/internal/util/mask.go:217:			MaskSecretsInStringMap			100.0%
github.com/ArmisSecurity/armis-cli/internal/util/path.go:13:			SanitizePath				90.9%
github.com/ArmisSecurity/armis-cli/internal/util/path.go:51:			SafeJoinPath				87.5%
github.com/ArmisSecurity/armis-cli/test/sample-repo/src/main.go:6:		main					0.0%
total:										(statements)				77.2%

Comment thread internal/agentdetect/detector.go Fixed
Comment thread internal/agentdetect/detector.go Fixed
Comment thread internal/agentdetect/detector.go Fixed
Comment thread internal/agentdetect/detector.go Fixed
Comment thread internal/agentdetect/detector.go Fixed
Comment thread internal/agentdetect/userprofile.go Fixed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new armis-cli agent-detection command to scan user profiles for installed AI coding agents and report whether Armis AppSec MCP is configured, and extends install to support additional editor targets.

Changes:

  • Introduces internal/agentdetect scanning framework with per-agent detectors, platform-specific user/profile discovery, and plain/JSON output formatters.
  • Adds the agent-detection Cobra command wiring and output format flag validation.
  • Extends installable editor targets to include Roo Code and Junie, and documents OpenHands as manual-only.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
internal/install/editors.go Adds Roo Code/Junie as auto-configurable editors and their default config paths.
internal/cmd/install.go Updates install help text and adds manual guidance for OpenHands.
internal/cmd/agent_detection.go Adds new agent-detection CLI command with --format support.
internal/agentdetect/platform.go Defines the Platform interface and UserHome model for cross-OS scanning.
internal/agentdetect/platform_darwin.go macOS implementation for user enumeration and editor-specific paths.
internal/agentdetect/platform_linux.go Linux implementation for user enumeration and editor-specific paths.
internal/agentdetect/platform_windows.go Windows implementation for user enumeration and editor-specific paths + admin detection.
internal/agentdetect/userprofile.go Shared helpers for enumerating home directories and JetBrains plugin paths.
internal/agentdetect/agent.go Defines agent names and the detector registry.
internal/agentdetect/agentdetect.go Implements the scanner and result models (grouped + flat).
internal/agentdetect/detector.go Implements per-agent detection, MCP checks, and some version extraction.
internal/agentdetect/mcpconfig.go Implements MCP presence checks for multiple config formats.
internal/agentdetect/format.go Implements plain and json output writers.
internal/agentdetect/agentdetect_test.go Unit tests for scanner aggregation and output formatters.
internal/agentdetect/detector_test.go Unit tests for each detector + path traversal safety checks.
internal/agentdetect/mcpconfig_test.go Unit tests for MCP config parsing helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/agentdetect/mcpconfig.go
Comment thread internal/agentdetect/platform_linux.go
Comment thread internal/agentdetect/format.go Outdated
Comment on lines +18 to +27
if _, err := fmt.Fprintf(w, "user: %s\n", userResult.User); err != nil {
return err
}
agents := make([]string, 0, len(userResult.Agents))
for _, agent := range userResult.Agents {
agents = append(agents, fmt.Sprintf("%s(MCP:%t)", agent.Name, agent.MCPInstalled))
}
if _, err := fmt.Fprintln(w, strings.Join(agents, ", ")); err != nil {
return err
}
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says the plain output is "one line per user", but this formatter prints two lines per user (a "user:" header line plus a second line listing agents). Either adjust the plain formatter to emit a single line per user, or update the command/README text to match the actual output format so downstream parsing expectations don't break.

Copilot uses AI. Check for mistakes.
Comment thread internal/cmd/agent_detection.go
Comment thread internal/agentdetect/detector.go Outdated
if err != nil {
return false
}
return strings.HasPrefix(resolved, resolvedBase+string(filepath.Separator))
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isUnderDir relies on a case-sensitive strings.HasPrefix comparison. On Windows (case-insensitive paths), this can produce false negatives if resolvedBase and resolved differ only by drive letter or casing, which would cause agent detection to miss valid paths. Consider switching to a filepath.Rel-based check (rel not starting with ".." and not ".") and, on Windows, normalizing/strings.EqualFold-ing the volume/case before comparison.

Suggested change
return strings.HasPrefix(resolved, resolvedBase+string(filepath.Separator))
baseVol := filepath.VolumeName(resolvedBase)
resolvedVol := filepath.VolumeName(resolved)
if baseVol != "" && resolvedVol != "" && strings.EqualFold(baseVol, resolvedVol) {
resolved = baseVol + strings.TrimPrefix(resolved, resolvedVol)
}
rel, err := filepath.Rel(resolvedBase, resolved)
if err != nil {
return false
}
return rel != "." && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))

Copilot uses AI. Check for mistakes.
Comment thread internal/agentdetect/userprofile.go
Comment thread internal/agentdetect/detector.go Outdated
}

func (d *clineDetector) CheckMCP(resolvedHome, homeDir string, _ Platform) bool {
return HasArmisMCP(resolvedHome, filepath.Join(homeDir, ".cline", "mcp_settings.json"))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clineDetector.CheckMCP() reads ~/.cline/mcp_settings.json, but armis-cli install cline writes to ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json. After a user installs and then runs detection, Cline will always report MCP:false — the two features disagree on where Cline stores its config.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's quite a bit of repeat here, I suggest defining an AgentConfig struct with Name, ConfigDirs []string, MCPConfigPath string, ExtensionPrefix string fields. Implement a generic detector that reads from config. Keep custom detectors (Claude Code's enabledPlugins, Continue's directory scanning) as special cases.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, there's definitely repetition here. I looked into a config-driven approach but ~half the agents have enough quirks (Claude's enabledPlugins, Continue's directory scanning, Zed's context_servers, Copilot reading from VSCode's config dir, etc.) that the generic struct would need several escape hatches. I'd prefer to keep the explicit detectors for now and revisit if/when we add more agents. Happy to discuss if you feel strongly about it though.

result := &ScanResult{}
for _, user := range users {
resolvedHome, err := resolvePath(user.HomeDir)
if err != nil {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When resolvePath(user.HomeDir) fails, the scan silently continues without any indication. Maybe add a Skipped []SkippedUser field to ScanResult or log warnings to stderr: fmt.Fprintf(os.Stderr, "warning: skipping user %s: %v\n", user.Username, err).?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plain formatter outputs raw fmt.Fprintf text while every other user-facing output in the project uses the lipgloss styling pipeline (documented in CLAUDE.md). The output looks disconnected from the rest of the CLI.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread internal/agentdetect/detector.go Outdated
return false
}
for _, entry := range entries {
if strings.Contains(strings.ToLower(entry.Name()), "armis") {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be filename-based only, while every other detector parses JSON content. A stale/empty armis-old.json or a armis-appsec.json.bak editor backup will falsely report "MCP installed = true".

am i missing something?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread internal/agentdetect/platform_linux.go Fixed
Comment thread internal/agentdetect/userprofile.go Fixed
Comment thread internal/agentdetect/detector.go Dismissed
Comment thread internal/agentdetect/detector.go Dismissed
Comment thread internal/agentdetect/detector.go Fixed
Comment thread internal/agentdetect/detector.go Fixed
Comment thread internal/agentdetect/detector.go Dismissed
Comment thread internal/agentdetect/mcpconfig.go Dismissed
Comment thread internal/agentdetect/platform_linux.go Dismissed
Comment thread internal/agentdetect/platform_linux.go Dismissed
Comment thread internal/agentdetect/userprofile.go Dismissed
Comment thread internal/agentdetect/userprofile.go Dismissed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants