Back to skill

Security audit

Engineering Manager Intelligence — Team Performance & Project Health

Security checks for vulnerabilities and agentic risk

Overview

The skill appears purpose-aligned, but it should be reviewed because it handles powerful service credentials with weak safeguards.

Review this before installing in a real workspace. Use dry-run or print mode first, keep the skill directory private, ensure .env is owner-readable only, use fine-grained least-privilege tokens with expirations, verify GitLab and Jira URLs are HTTPS and trusted, and avoid entering tokens in recorded or shared terminals.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
Findings (6)

T09 · Insecure Skill Coding Practices

Error
Location
adapters/gitlab_adapter.py:25
Finding

GitLab access token can be transmitted to an arbitrary or plaintext endpoint

Content
View full analysis
list: """Paginated GET helper. Returns all results across pages.""" results: list = [] params = params or {} params.setdefault("per_page", 100) page = 1 while True: params["page"] = page try: resp = requests.get( f"{self.url}/api/v4{path}", headers=self.headers, params=params, timeout=30, ) resp.raise_for_status() ``` The same behavior is present in the API connection check at `em_intel.py:135-143`. ### Technical Analysis `GITLAB_URL` is accepted without validating its scheme, hostname, port, or destination. The application then attaches the GitLab personal access token as a `PRIVATE-TOKEN` header to requests made to that configured URL. A configuration mistake or malicious modification of `.env` can therefore redirect the credential-bearing request to an attacker-controlled server. The code also permits an `http://` URL, allowing the token and returned project information to traverse the network without transport encryption. Although configurable GitLab endpoints are needed to support self-hosted GitLab installations, sending credentials to an unrestricted URL exceeds safe endpoint configurability. ### Attack Path 1. An attacker modifies `.env`, supplies a deceptive setup value, or convinces the user to configure `GITLAB_URL` as an attacker-controlled endpoint. 2. The user runs `doctor`, `morning-brief`, `team-report`, or ...[truncated 613 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
adapters/jira_adapter.py:25
Finding

Jira credentials can be transmitted to an arbitrary or plaintext endpoint

Content
View full analysis
list: """Run a JQL search with pagination.""" results: list = [] start = 0 while True: try: resp = requests.get( f"{self.url}/rest/api/3/search/jql", headers=self.headers, params={ "jql": jql, "fields": fields, "startAt": start, "maxResults": min(max_results - len(results), 100), }, timeout=30, ) resp.raise_for_status() ``` The same behavior is present in the Jira connection check at `em_intel.py:201-216`. ### Technical Analysis The Jira URL is configuration-controlled and is not validated before the application sends an HTTP Basic authorization header containing the user's email address and API token. Base64 encoding does not protect the credentials; it is reversible encoding rather than encryption. This configurability is necessary to support different Jira tenants, but unrestricted schemes and destinations are not necessary. In particular, accepting plaintext HTTP or an attacker-controlled host creates a direct credential-disclosure path. ### Attack Path 1. An attacker modifies or supplies `JIRA_URL`, setting it to an attacker-controlled HTTP or HTTPS server. 2. The user runs `doctor` or a command that fetches Jira tickets or epics. 3. The applica ...[truncated 560 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
commands/setup_wizard.py:25
Finding

Setup wizard visibly echoes authentication secrets

Content
View full analysis
str: """Prompt for a secret value (shown as-is since we're in a terminal).""" value = input(f"{prompt}: ").strip() return value ``` This function is used for GitLab tokens, GitHub tokens, Jira tokens, SMTP passwords, Slack webhook URLs, and Telegram bot tokens. ### Technical Analysis Python's `input()` echoes entered characters to the terminal. Authentication secrets therefore remain visible while they are typed and may be captured by terminal session recording, screen sharing, shell-hosting services, shoulder surfing, or terminal scrollback facilities. The fact that setup runs in a terminal does not make visible secret entry safe. Python provides `getpass.getpass()` specifically to disable terminal echo for passwords and tokens. ### Attack Path 1. A user runs the setup wizard in a shared, recorded, streamed, or observable terminal. 2. The wizard requests a token or password using `_ask_secret`. 3. Every character is displayed as the user types it. 4. An observer or recording captures the credential. 5. The credential is reused against GitHub, GitLab, Jira, SMTP, Slack, or Telegram. ### Impact Assessment The exposed privilege depends on the captured secret. It may include private source-code access, Jira project access, email account or SMTP relay access, Slack webhook posting capability, or Telegram bot control. The broad GitHub token scope requested by the setup instructions increases the potential repository impact. ]]>
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
commands/setup_wizard.py:199
Finding

Generated credential file is not explicitly restricted to the owner

Content
View full analysis
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
em_intel.py:16
Finding

Normal execution automatically installs unpinned third-party dependencies

Content
View full analysis
None: """Install requirements silently on first run if any are missing.""" req = SKILL_DIR / "requirements.txt" if not req.exists(): return try: import importlib needed = {"requests", "dotenv", "rich", "jinja2", "markdown"} missing = [p for p in needed if not importlib.util.find_spec(p)] if missing: subprocess.run( [sys.executable, "-m", "pip", "install", "-r", str(req), "-q"], check=False, capture_output=True, ) except Exception: pass # Don't block startup _bootstrap_deps() ``` The dependency declarations at `requirements.txt:1-5` are: ```text requests>=2.31.0 python-dotenv>=1.0.0 rich>=13.0.0 jinja2>=3.1.0 markdown>=3.5.0 ``` The launcher also invokes pip on every Python execution at `run.sh:43-45`. ### Technical Analysis The application silently invokes pip during ordinary startup when it detects a missing import. The shell launcher invokes pip even before that check. Dependencies use lower-bound constraints rather than exact, reviewed versions and are not protected by package hashes. Consequently, later package versions may be installed without code review or explicit user approval. Python package installation can execute package build and installation logic with the privileges of the invoking user. This creates a supply-chain exposure if a dependency, transitive dependency, or configured package index is compromised. ### Attack Path 1. A listed package, transitive dependency, or package-index account is compromised, or a malicious future release satisfies the lower-bound constraint. 2. A user launches the Skill in an environment where a dependency is absent, or uses `run.sh`. 3. The Skill ...[truncated 523 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
commands/setup_wizard.py:129
Finding

Setup instructions request an overprivileged GitHub access token

Content
View full analysis
2. Name: `em-intel` 3. Scopes: **`repo`**, **`read:org`** 4. Click **Generate token** and copy the value ``` ### Technical Analysis The GitHub adapters perform only read operations through HTTP GET requests. However, the setup documentation directs users to create a classic personal access token with the broad `repo` scope. That scope grants extensive access to private repositories and includes capabilities beyond the read-only metadata, pull-request, branch, commit, issue, and milestone access required by this Skill. Requesting a broad classic scope violates least privilege and magnifies the impact of the plaintext entry and local credential-storage weaknesses. ### Attack Path 1. The user follows the setup wizard or `SETUP.md`. 2. The user creates a classic GitHub personal access token with `repo` and `read:org`. 3. The token is stored in `.env`. 4. The token is exposed through visible entry, weak local permissions, malware, backup leakage, or another local compromise. 5. The attacker uses privileges that exceed the Skill's read-only functional requirements. ### Impact Assessment A stolen token may grant broad access across private repositories available to the user's account, potentially including write-capable operations and access to sensitive repository resources. The exact scope depends on organization controls and the user's existing GitHub privileges, but it is materially broad ...[truncated 31 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (54)

Tainted flow: 'jql' from os.getenv (line 69, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · adapters/jira_adapter.py (reported line 40)May include surrounding context.

python
start = 0
        while True:
            try:
                resp = requests.get(
                    f"{self.url}/rest/api/3/search/jql",
                    headers=self.headers,
                    params={

YARA rule 'agent_skill_credential_exfiltration_webhook': AI agent skill credential harvesting followed by webhook or external exfiltration [agent_skills]

Critical
Category
YARA Match
Confidence
85% confidence
Finding

YARA rule matched a known malware signature (reverse shell, backdoor, ransomware, C2 framework, or info stealer).

Content

Scanner excerpt · core/delivery.py (reported line 23)May include surrounding context.

python
.mime.text import MIMEText

import requests

logger = logging.getLogger(__name__)


def send(text: str, title: str = "EM Intel Report") -> bool:
    """Route a message to the configured delivery channel.

    Reads EM_DELIVERY env var to determine channel.
    Falls back to print if channel is unset or fails.

    Returns:
        True if delivery succeeded, False otherwise.
    """
    channel = os.getenv("EM_DELIVERY", "print").lower()

    if channel == "telegram":
        return _send_telegram(text, title)
    elif channel == "slack":
        return _send_slack(text, title)
    elif channel == "email":
        return _send_email(text, title)
    else:
        return _send_print(text, title)


def _send_telegram(text: str, title: str) -> bool:
    """Send via Telegram Bot API."""
    token = os.getenv("TELEGRAM_BOT_TOKEN", "")
    chat_id = os.getenv("TELEGRAM_CHAT_ID", "")
    if not token or not chat_id:
        logger.warning("Telegram credentials not configured, falling back to

Tainted flow: 'token' from os.getenv (line 37, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · core/delivery.py (reported line 49)May include surrounding context.

python
message = message[:4090] + "\n..."

    try:
        resp = requests.post(
            f"https://api.telegram.org/bot{token}/sendMessage",
            json={
                "chat_id": chat_id,

Tainted flow: 'webhook_url' from os.getenv (line 68, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · core/delivery.py (reported line 74)May include surrounding context.

python
return _send_print(text, title)

    try:
        resp = requests.post(
            webhook_url,
            json={"text": f"*{title}*\n\n{text}"},
            timeout=15,

Tainted flow: 'url' from os.getenv (line 134, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

The GitLab URL is taken directly from environment configuration and used in an authenticated outbound request with the PRIVATE-TOKEN header. If an attacker can influence environment variables or setup values, they can redirect the health check to an attacker-controlled endpoint and capture the GitLab token, resulting in SSRF-style token exfiltration.

Content

Scanner excerpt · em_intel.py (reported line 137)May include surrounding context.

python
url = env_checks["GITLAB_URL"].rstrip("/")
            group = quote_plus(env_checks["GITLAB_GROUP"])
            try:
                resp = requests.get(
                    f"{url}/api/v4/groups/{group}/merge_requests",
                    headers={"PRIVATE-TOKEN": env_checks["GITLAB_TOKEN"]},
                    params={"per_page": 1, "state": "merged"},

Tainted flow: 'org' from os.getenv (line 162, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · em_intel.py (reported line 165)May include surrounding context.

python
org = env_checks["GITHUB_ORG"]
            repo = env_checks["GITHUB_REPO"]
            try:
                resp = requests.get(
                    f"https://api.github.com/repos/{org}/{repo}",
                    headers={
                        "Authorization": f"Bearer {env_checks['GITHUB_TOKEN']}",

Tainted flow: 'jira_url' from os.getenv (line 201, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
94% confidence
Finding

The Jira base URL comes from environment configuration and is used to send a Basic Authorization header derived from email and API token. If that URL is maliciously set or altered, the doctor command will transmit reusable Jira credentials to an attacker-controlled server, making this more severe than a normal API call.

Content

Scanner excerpt · em_intel.py (reported line 207)May include surrounding context.

python
).decode()
            project = env_checks_t["JIRA_PROJECTS"].split(",")[0].strip()
            try:
                resp = requests.get(
                    f"{jira_url}/rest/api/3/search/jql",
                    headers={
                        "Authorization": f"Basic {creds}",

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SETUP.md (reported line 13)May include surrounding context.

  1. Click Add new token
  2. Name: em-intel (or similar)
  3. Scope: read_api
  4. Click Create personal access token and copy the value
env
EM_CODE_PROVIDER=gitlab

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

Yes, this is a mismatch based on the supplied chunk. The description claims a broad engineering-intelligence skill with repository/issue tracker integrations, feature-branch analysis, reporting, alerting, and multi-channel delivery. The actual code chunk contains only a package docstring: 'CLI command implementations for em-intel.' There is no executable behavior to substantiate the declared functionality. While this may be just a tiny fragment of a larger project, evaluating only the provided code means the declared purpose is not accurately represented here. The only observable behavior/purpose implied by the code is that it is a CLI commands package, which is materially narrower and different from the richly described operational skill.

Content

No source excerpt is available for this finding.

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 25)May include surrounding context.

1. Check if configured:

bash
test -f <skill_dir>/.env && echo "configured" || echo "not_configured"

If not configured, say:

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 53)May include surrounding context.

1. Check if configured:

bash
test -f <skill_dir>/.env && echo "configured" || echo "not_configured"

If not configured, say:

Credential Access

High
Category
Privilege Escalation
Confidence
95% confidence
Finding

The wizard is explicitly designed to collect API tokens and store them in .env, so this is a true credential-handling concern rather than a false alarm. In the context of an agent skill, centralizing code-hosting, Jira, email, Slack, and Telegram secrets in one local plaintext file increases blast radius if the file is exposed.

Content

Scanner excerpt · commands/setup_wizard.py (reported line 84)May include surrounding context.

python
def run() -> None:
    """Run the interactive setup wizard."""
    env_path = SKILL_DIR / ".env"

    print("\n" + "═" * 60)
    print("  em-intel  Setup Wizard")

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · commands/setup_wizard.py (reported line 120)May include surrounding context.

python
code_provider = _ask("  Platform", default="gitlab", choices=["gitlab", "github"])

    if code_provider == "gitlab":
        print("\n  You'll need a GitLab Personal Access Token with 'read_api' scope.")
        _open_url(
            "https://gitlab.com/-/user_settings/personal_access_tokens",
            "GitLab token page"

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · commands/setup_wizard.py (reported line 130)May include surrounding context.

python
code_provider = _ask("  Platform", default="gitlab", choices=["gitlab", "github"])

    if code_provider == "gitlab":
        print("\n  You'll need a GitLab Personal Access Token with 'read_api' scope.")
        _open_url(
            "https://gitlab.com/-/user_settings/personal_access_tokens",
            "GitLab token page"

Credential Access

High
Category
Privilege Escalation
Confidence
96% confidence
Finding

Step 5 writes all collected credentials to disk in one operation, confirming persistent plaintext credential storage. Because this skill interfaces with several external systems, compromise of the generated file could expose source control, issue tracker, messaging, and email access together.

Content

Scanner excerpt · commands/setup_wizard.py (reported line 189)May include surrounding context.

python
telegram_token = _ask_secret("  Telegram bot token")
        telegram_chat_id = _ask("  Telegram chat ID (your user or group ID)")

    # ── Step 5: Write .env ────────────────────────────────────────────
    print("\nStep 5/5 — Writing configuration...")

    lines = [

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · em_intel.py (reported line 510)May include surrounding context.

python
set -e
SKILL_DIR="$(cd "$(dirname "$0")" && pwd)"
IMAGE_NAME="em-intel"
ENV_FILE="$SKILL_DIR/.env"

# ── Detect runtime ─────────────────────────────────────────────────────────────
HAS_PYTHON=false

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · run.sh (reported line 13)May include surrounding context.

sh
set -e
SKILL_DIR="$(cd "$(dirname "$0")" && pwd)"
IMAGE_NAME="em-intel"
ENV_FILE="$SKILL_DIR/.env"

# ── Detect runtime ─────────────────────────────────────────────────────────────
HAS_PYTHON=false

Credential Access

High
Category
Privilege Escalation
Confidence
95% confidence
Finding

Checking for the existence of '.env' and then passing it wholesale to 'docker run --env-file' enables the container to access any secrets stored there. In the context of an engineering-intelligence skill that likely uses repository, issue tracker, chat, and email credentials, this materially increases the blast radius if the containerized code is flawed, compromised, or unexpectedly logs/transmits environment variables.

Content

Scanner excerpt · run.sh (reported line 57)May include surrounding context.

sh
echo "✓  Image ready"
fi

# Run with .env if it exists
if [ -f "$ENV_FILE" ]; then
  exec docker run --rm --env-file "$ENV_FILE" "$IMAGE_NAME" "$@"
else

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The setup guide includes numerous secrets and sensitive integration values such as API tokens, SMTP credentials, Slack webhook URLs, and Telegram bot tokens, but does not explicitly warn users to keep them secret or avoid committing them to source control. In a setup document for an automation skill that aggregates engineering and ticketing data, this omission materially increases the chance of accidental credential leakage and unauthorized access to internal repositories, issue trackers, and messaging channels.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding

The skill instructs the agent to use shell, access environment-backed configuration, install dependencies, and invoke a Python CLI, but it does not declare any explicit tool scope or permission boundaries. That creates an authorization gap where a host agent may execute networked or filesystem-affecting actions without the user being clearly warned or the platform enforcing least privilege.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The skill handles engineering activity, tickets, and reporting through external services including Slack, Telegram, and email, but the description does not clearly warn users that sensitive team/project metadata may be transmitted off-platform. This increases the risk of unintentional disclosure of internal operational data, especially when newsletters or reports are delivered to third-party channels.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The trigger list includes broad natural-language phrases like "who worked on X," "which tickets are stalled," and "team performance," which can match ordinary conversation and cause accidental invocation. In this skill's context, accidental activation is more dangerous because execution may lead to shell commands, API access, and outbound reporting of team/project data.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · commands/setup_wizard.py (reported line 45)May include surrounding context.

python
def _detect_runtime() -> str:
    """Return 'python', 'docker', or 'none'."""
    try:
        result = subprocess.run(
            [sys.executable, "-c", "import sys; sys.exit(0 if sys.version_info>=(3,9) else 1)"],
            capture_output=True,
        )

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · commands/setup_wizard.py (reported line 54)May include surrounding context.

python
except Exception:
        pass
    try:
        result = subprocess.run(
            ["docker", "info"], capture_output=True, timeout=5
        )
        if result.returncode == 0:

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
84% confidence
Finding

The setup wizard automatically runs pip install -r requirements.txt, which executes a network-dependent package installation during setup without pinning, verification, or explicit trust checks. If the dependency file or package source is tampered with, this can introduce malicious code or a compromised package into the local environment.

Content

Scanner excerpt · commands/setup_wizard.py (reported line 72)May include surrounding context.

python
if not req_file.exists():
        return True
    try:
        result = subprocess.run(
            [sys.executable, "-m", "pip", "install", "-r", str(req_file), "-q"],
            capture_output=True,
            text=True,

Static analysis

No suspicious patterns detected.