From 5b66e655f3fb3178377adedf884ce82791e0991e Mon Sep 17 00:00:00 2001 From: Brahadeesh V Ra <144269250+CodeforGood1@users.noreply.github.com> Date: Sun, 10 May 2026 01:15:51 +0530 Subject: [PATCH 1/4] Refactor ContractGuard into VS Code extension package --- .github/workflows/contractguard-ci.yml | 38 +- .gitignore | 2 + CAPABILITIES.md | 560 +-- DEPLOYMENT.md | 282 +- INSTRUCTIONS.md | 430 +- README.md | 277 +- media/icon.png | Bin 0 -> 1385 bytes media/icon.svg | 5 + package-lock.json | 4001 +++++++++++++++++++ package.json | 170 + pyproject.toml | 10 +- python-requirements.txt | 9 + rules/csv_rules.yaml | 42 - samples/config/dangerous.env | 8 +- samples/csv/users.csv | 16 - samples/secrets/leaked.env | 39 +- src/contractguard/__init__.py | 4 +- src/contractguard/analyzers/csv_analyzer.py | 154 - src/contractguard/bridge.py | 44 + src/contractguard/cli.py | 276 +- src/contractguard/reporter.py | 275 +- src/contractguard/scan.py | 187 + src/contractguard/web.py | 164 - tests/test_bridge.py | 34 + tests/test_csv_analyzer.py | 79 - tests/test_scan.py | 45 + tests/test_secrets_analyzer.py | 28 +- tsconfig.json | 15 + vscode-src/extension.ts | 373 ++ vscode-src/findingsTree.ts | 86 + vscode-src/pythonBridge.ts | 154 + vscode-src/types.ts | 38 + 32 files changed, 5525 insertions(+), 2320 deletions(-) create mode 100644 media/icon.png create mode 100644 media/icon.svg create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 python-requirements.txt delete mode 100644 rules/csv_rules.yaml delete mode 100644 samples/csv/users.csv delete mode 100644 src/contractguard/analyzers/csv_analyzer.py create mode 100644 src/contractguard/bridge.py create mode 100644 src/contractguard/scan.py delete mode 100644 src/contractguard/web.py create mode 100644 tests/test_bridge.py delete mode 100644 tests/test_csv_analyzer.py create mode 100644 tests/test_scan.py create mode 100644 tsconfig.json create mode 100644 vscode-src/extension.ts create mode 100644 vscode-src/findingsTree.ts create mode 100644 vscode-src/pythonBridge.ts create mode 100644 vscode-src/types.ts diff --git a/.github/workflows/contractguard-ci.yml b/.github/workflows/contractguard-ci.yml index afaef89..52177d9 100644 --- a/.github/workflows/contractguard-ci.yml +++ b/.github/workflows/contractguard-ci.yml @@ -10,11 +10,12 @@ permissions: contents: read jobs: - contractguard: + validate: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.11", "3.12"] + node-version: ["20"] steps: - uses: actions/checkout@v4 @@ -24,32 +25,31 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Set up Node ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install Python dependencies run: | python -m pip install --upgrade pip pip install -e ".[dev]" - - name: Run unit tests - run: pytest tests/ -v --tb=short + - name: Install extension dependencies + run: npm ci - - name: Analyze JSON samples - run: | - contractguard analyze --type json --path samples/json/ \ - --report report-json.html + - name: Run Python tests + run: pytest tests/ -v --tb=short - - name: Analyze SQL samples - run: | - contractguard analyze --type sql --path samples/sql/ \ - --report report-sql.html + - name: Build extension + run: npm run build - - name: Analyze Regex samples - run: | - contractguard analyze --type regex --path samples/regex/ \ - --report report-regex.html + - name: Package VSIX + run: npm run package - - name: Upload HTML reports + - name: Upload VSIX if: always() uses: actions/upload-artifact@v4 with: - name: contractguard-reports-${{ matrix.python-version }} - path: report-*.html + name: contractguard-vsix-${{ matrix.python-version }} + path: dist-vsix/*.vsix diff --git a/.gitignore b/.gitignore index d1ea650..e33ac99 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ __pycache__/ dist/ build/ *.egg +dist-vsix/ +node_modules/ # Virtual environments .venv/ diff --git a/CAPABILITIES.md b/CAPABILITIES.md index 4d994b1..c6350db 100644 --- a/CAPABILITIES.md +++ b/CAPABILITIES.md @@ -1,545 +1,27 @@ -# ContractGuard — Full Capabilities Reference +# Capabilities Reference -> Version 2.0.0 | 9 Analyzers | 57+ Rules | 162 Tests +## Core engine ---- +- Rule-driven analyzer execution +- Shared findings and severity model +- Security scoring and grade calculation +- HTML reporting +- SARIF 2.1.0 export +- Scan history storage +- Machine-readable bridge for editor integration -## Table of Contents +## VS Code integration -1. [Analyzer Capabilities](#1-analyzer-capabilities) -2. [Security Scoring](#2-security-scoring) -3. [Reporting Formats](#3-reporting-formats) -4. [CLI Commands](#4-cli-commands) -5. [Web UI](#5-web-ui) -6. [Rule Engine](#6-rule-engine) -7. [CI/CD Integration](#7-cicd-integration) -8. [Scan History & Trends](#8-scan-history--trends) -9. [Severity Model](#9-severity-model) -10. [Compliance Coverage](#10-compliance-coverage) +- Workspace and file scan commands +- Diagnostics per file +- Findings explorer +- Status bar score +- Scan-on-save +- SARIF export command +- Disabled-rule filtering in the client ---- +## Non-goals in this repository -## 1. Analyzer Capabilities - -### 1.1 JSON Schema Analyzer (`--type json`) - -Scans JSON files and infers schema facts from the actual data, then checks those facts against rules. - -| Capability | Detail | -|------------|--------| -| Type inference | Detects integer, number, string, boolean, null, array, object per field | -| Type drift detection | Flags fields that hold more than one type across records | -| Nullable field detection | Reports fields that are null in some records | -| Structural consistency | Checks field presence vs absence across object arrays | -| Schema violations | Catches empty arrays, deeply nested objects, unexpected schema shapes | -| Multi-file support | Scans every `.json` file in a directory | - -**Rules:** 8 rules (JSON001–JSON008) - ---- - -### 1.2 SQL Query Analyzer (`--type sql`) - -Static analysis of SQL queries for performance anti-patterns and security issues. - -| Capability | Detail | -|------------|--------| -| `SELECT *` detection | Flags queries selecting all columns | -| Missing `WHERE` clause | Detects full-table reads on `DELETE`, `UPDATE`, `SELECT` | -| Cartesian join detection | Finds `JOIN` without `ON` conditions | -| Subquery in `WHERE` | Flags correlated subquery patterns | -| Leading-wildcard `LIKE` | Detects `LIKE '%value'` which defeats indexes | -| `OR` in `WHERE` | Flags conditions that may prevent index use | -| Multi-statement detection | Warns on semicolon-separated statement chains | -| Optional EXPLAIN support | Can run `EXPLAIN` against a live SQLite DB | - -**Rules:** 8 rules (SQL001–SQL008) - ---- - -### 1.3 Regex Complexity Analyzer (`--type regex`) - -Parses regex patterns using Python's internal `re._parser` AST and scores structural complexity. - -| Capability | Detail | -|------------|--------| -| Complexity scoring | 0–100 score based on nested quantifiers, alternations, groups | -| ReDoS detection | Flags nested quantifiers (`(a+)+`, `(a*)*`) — catastrophic backtracking | -| Backreference detection | Warns on `\1`, `\2` style backreferences | -| Long alternation chains | Detects alternation with 10+ branches | -| Syntax validation | Catches invalid regex patterns outright | -| Pattern length | Flags patterns over 200 characters | - -**Rules:** 7 rules (REG001–REG007) - ---- - -### 1.4 Secrets Detection Analyzer (`--type secrets`) - -Scans any file for hardcoded credentials using 21 regex patterns. All matched values are **redacted** in output. - -| Pattern | What It Catches | -|---------|----------------| -| AWS Access Key | `AKIA[0-9A-Z]{16}` format | -| AWS Secret Key | 40-char alphanumeric after `aws_secret` keys | -| GitHub Token | `ghp_`, `gho_`, `ghs_`, `github_pat_` prefixes | -| Stripe Secret Key | `sk_live_` and `sk_test_` | -| Stripe Publishable Key | `pk_live_` and `pk_test_` | -| Slack Token | `xoxb-`, `xoxp-`, `xoxa-` prefixes | -| GCP API Key | `AIza[0-9A-Za-z]{35}` | -| Generic API Key | `api[_-]?key\s*=\s*` patterns | -| Database URLs | `postgresql://`, `mysql://`, `mongodb://` with credentials | -| Private Keys | PEM `-----BEGIN PRIVATE KEY-----` headers | -| RSA Private Key | PEM `-----BEGIN RSA PRIVATE KEY-----` headers | -| JWT Tokens | Three-segment base64 with `.` separators | -| npm Auth Token | `_authToken` in `.npmrc` | -| SendGrid API Key | `SG.[a-zA-Z0-9]{22}` format | -| Twilio Auth Token | 32-char hex after `twilio` keyword | -| Generic Passwords | `password\s*=\s*` with non-placeholder values | -| Hex Secrets | 32–64 char hex strings in secret/token fields | -| Base64 Secrets | Long base64 blobs in secret/key fields | -| Bearer Tokens | `Authorization: Bearer ...` headers | -| Hardcoded Auth Headers | Auth header values in source files | -| Generic Secret Fields | `secret\s*=\s*` or `token\s*=\s*` patterns | - -**Rules:** 5 rules (SEC001–SEC005) | Default severity: **BLOCK / CRITICAL** - ---- - -### 1.5 PII Detection Analyzer (`--type pii`) - -Detects personally identifiable information across any file type. Maps findings to GDPR, CCPA, and HIPAA. - -| Pattern | Regulation | Severity | -|---------|-----------|----------| -| US Social Security Number (SSN) | HIPAA, CCPA | **BLOCK** | -| Credit / Debit Card Numbers | PCI-DSS, CCPA | **BLOCK** | -| Email Addresses | GDPR, CCPA | CRITICAL | -| US Phone Numbers | CCPA | CRITICAL | -| Date of Birth | GDPR, HIPAA | CRITICAL | -| IP Addresses | GDPR | WARNING | -| Passport Numbers | GDPR | CRITICAL | -| IBAN / Bank Account Numbers | GDPR | CRITICAL | -| Driver's License Numbers | CCPA | CRITICAL | -| Medical Record Numbers (MRN) | HIPAA | **BLOCK** | -| NHS / National Health IDs | GDPR | CRITICAL | -| PII Field Name Detection | All | WARNING | - -Also detects PII by **field name** heuristics: fields named `ssn`, `dob`, `passport`, `credit_card`, `medical_id`, etc. - -**Rules:** 4 rules (PII001–PII004) - ---- - -### 1.6 CSV Data Quality Analyzer (`--type csv`) - -Audits CSV files for structural and data quality issues. - -| Capability | Detail | -|------------|--------| -| Type inference per column | Classifies each column: null / boolean / integer / number / date / string | -| Mixed-type columns | Flags columns where values resolve to more than one type | -| Inconsistent column counts | Detects rows with different field counts than the header | -| Null-heavy columns | Flags columns with more than 30% null/empty values | -| Duplicate rows | Detects identical rows across the dataset | -| Encoding issues | Catches non-UTF-8 characters suggesting encoding corruption | -| Large file warning | Notes very large CSVs that may cause memory issues | - -**Rules:** 5 rules (CSV001–CSV005) - ---- - -### 1.7 Config Security Analyzer (`--type config`) - -Audits configuration files for dangerous settings. Supports `.yaml`, `.yml`, `.toml`, `.json`, `.env`, `.ini`, `.cfg`, `.conf`. - -| Check | What It Detects | Severity | -|-------|----------------|----------| -| Debug mode enabled | `DEBUG=true`, `debug: true` | CRITICAL | -| CORS wildcard | `CORS_ALLOW_ALL`, `allow_origins: ["*"]` | CRITICAL | -| Insecure secret key | Weak/default `SECRET_KEY` values | **BLOCK** | -| Default passwords | `password=admin`, `password=12345`, etc. | **BLOCK** | -| SSL/TLS disabled | `ssl_enabled: false`, `tls=off`, `https=disabled` | CRITICAL | -| Wildcard host binding | `HOST=0.0.0.0` in non-container configs | WARNING | -| Root user | `DB_USER=root` or `user=root` entries | CRITICAL | -| HTTP instead of HTTPS | `http://` URLs in production configs | WARNING | -| Exposed admin ports | Common admin ports (8080, 9200, 5601, 15672) | WARNING | - -**Rules:** 9 rules (CFG001–CFG009) - ---- - -### 1.8 Dockerfile Analyzer (`--type dockerfile`) - -Static analysis of Dockerfiles for security misconfigurations and bad practices. - -| Check | What It Detects | Severity | -|-------|----------------|----------| -| Runs as root | No `USER` instruction set | CRITICAL | -| Latest tag | `FROM image:latest` (non-deterministic builds) | WARNING | -| `COPY .` (broad copy) | Copies entire build context including secrets | WARNING | -| Hardcoded secrets in ENV | `ENV API_KEY=...`, `ENV PASSWORD=...` | **BLOCK** | -| `curl \| bash` installs | Remote code execution at build time | CRITICAL | -| Exposed SSH port | `EXPOSE 22` opens SSH | CRITICAL | -| `sudo` usage | Privilege escalation inside container | WARNING | -| No `HEALTHCHECK` | Missing container health monitoring | INFO | -| Too many `RUN` layers | More than 10 `RUN` commands (image bloat) | INFO | - -**Rules:** 8 rules (DOCK001–DOCK008) - ---- - -### 1.9 Dependency Vulnerability Analyzer (`--type deps`) - -Scans `requirements.txt` against a built-in offline vulnerability database — **no API key, no network request**. - -**Coverage:** 29 vulnerability entries across 15 packages - -| Package | Vulnerable Versions | Issue | -|---------|-------------------|-------| -| Django | <2.2.28, <3.2.15, <4.1.2 | Multiple CVEs (SQL injection, XSS, CSRF bypass, auth bypass) | -| Flask | <1.0.0 | Debug mode exposure, known security issues | -| Requests | <2.20.0 | CVE-2018-18074 — credential exposure on redirect | -| urllib3 | <1.24.2 | CVE-2019-11324 — certificate verification bypass | -| cryptography | <41.0.0 | NULL pointer deref, memory corruption | -| Jinja2 | <2.11.3 | CVE-2020-28493 — ReDoS vulnerability | -| PyYAML | <5.4 | CVE-2020-14343 — arbitrary code execution | -| sqlparse | <0.4.4 | CVE-2023-30608 — ReDoS vulnerability | -| aiohttp | <3.8.5 | CVE-2023-37276 — request smuggling | -| FastAPI | <0.95.0 | Dependency confusion, known security patches | -| Werkzeug | <2.2.3 | CVE-2023-23934, CVE-2023-25577 — cookie injection, DoS | -| Paramiko | <2.10.1 | CVE-2022-24302 — private key file race condition | -| Pillow | <9.3.0 | Multiple CVEs — buffer overflow, arbitrary code exec | -| lxml | <4.9.1 | CVE-2022-2309 — NULL deref | -| Setuptools | <65.5.1 | CVE-2022-40897 — ReDoS | - -Also detects **unpinned dependencies** (no version specifier) as a WARNING. - -**Rules:** 3 rules (DEP001–DEP003) | Critical CVEs severity: **BLOCK** - ---- - -### 1.10 Full Project Scan (`--type all`) - -Runs all 9 analyzers in a single pass across an entire directory tree. - -- Recursively discovers all relevant file types -- Deduplicates findings per file -- Produces a unified finding list with analyzer attribution -- Computes a single security score across all findings -- Generates one combined HTML, JSON, or SARIF report - -```bash -contractguard analyze --type all --path . --report report.html --score -``` - ---- - -## 2. Security Scoring - -Every scan produces an **A–F security grade** and a **0–100 numeric score**. - -### Grade Scale - -| Grade | Score | Meaning | -|-------|-------|---------| -| A | 90–100 | Production ready | -| B | 75–89 | Minor issues to address | -| C | 60–74 | Moderate risk — review before deploy | -| D | 40–59 | Significant issues — do not deploy | -| F | 0–39 | Critical failures — deployment blocked | - -### Score Deductions - -| Severity | Deduction per Finding | -|----------|----------------------| -| BLOCK | 20 points + automatic F | -| CRITICAL | 10 points | -| WARNING | 4 points | -| INFO | 1 point | - -> Any single **BLOCK** finding forces the grade to **F** regardless of total score. - -### Score Output Includes - -- Letter grade (A–F) -- Numeric score (0–100) -- Finding counts per severity -- Attack surface list (unique attack vectors identified) -- Top risks summary (highest-severity findings) - ---- - -## 3. Reporting Formats - -### 3.1 HTML Report (`--report report.html`) - -- Dark theme with aggressive security posture styling -- **Security grade banner** — large A–F circle with color coding (green → red) -- Attack surface section — lists all unique attack vectors found -- Top risks section — top 5 highest-severity findings -- Full findings table with: Severity badge, Rule ID, Name, File, Description, CWE, Attack Vector, Suggestion -- BLOCK severity findings pulse/animate to draw attention -- Summary statistics bar - -### 3.2 JSON Report (`--report-json report.json`) - -Structured machine-readable output: - -```json -{ - "summary": { "total": 164, "block": 30, "critical": 130, "warning": 4, "info": 0 }, - "score": { "grade": "F", "score": 0 }, - "findings": [ - { - "rule_id": "SEC001", - "name": "hardcoded_secrets", - "severity": "block", - "file": "config/.env", - "description": "...", - "suggestion": "...", - "cwe": "CWE-798", - "attack_vector": "..." - } - ] -} -``` - -### 3.3 SARIF 2.1.0 (`--report-sarif results.sarif`) - -Fully compliant [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) output for direct upload to: - -- **GitHub Code Scanning** (Security tab → Code scanning alerts) -- **Azure DevOps** Security scans -- Any SARIF-compatible viewer - -SARIF output includes: -- `$schema` and `version: "2.1.0"` -- `tool.driver` with all rules, CWE taxa references -- `results` with per-finding locations, message, level, and rule reference -- Physical location with `artifactLocation.uri` and `region.startLine` -- CWE taxonomy references in `taxa` - ---- - -## 4. CLI Commands - -### `analyze` — Core analysis command - -``` -contractguard analyze [OPTIONS] - - -t, --type TEXT json|sql|regex|secrets|pii|csv|config|dockerfile|deps|all - -p, --path PATH File or directory to scan - --report PATH Write HTML report - --report-json PATH Write JSON report - --report-sarif PATH Write SARIF 2.1.0 report - --score Print security grade after scan - --record Save scan to history database - --ci Exit code 2 on critical/block findings -``` - -### `score` — Grade your entire project - -``` -contractguard score [OPTIONS] - -p, --path PATH Directory to scan (default: .) - -Runs all 9 analyzers and displays a security grade panel. -``` - -### `history` — View scan trends - -``` -contractguard history [OPTIONS] - --limit INT Number of past scans to show (default: 10) - --db PATH Path to history database -``` - -### `watch` — Continuous scanning - -``` -contractguard watch [OPTIONS] - -p, --path PATH Directory to watch - -t, --type TEXT Analyzer type (default: all) - --interval INT Seconds between scans (default: 30) - -Re-runs the specified analyzer every N seconds. Useful during development. -``` - -### `serve` — Launch web UI - -``` -contractguard serve -Opens http://127.0.0.1:8000 — file upload UI with all 9 analyzer types. -``` - -### `version` — Show version - -``` -contractguard version -``` - ---- - -## 5. Web UI - -A FastAPI-powered single-page application accessible at `http://127.0.0.1:8000`. - -| Feature | Detail | -|---------|--------| -| File upload | Upload any file directly from the browser | -| Analyzer selection | Dropdown with all 9 types + "all" | -| Inline results | Findings table rendered directly on the page | -| Dark theme | Consistent with CLI and HTML reports | -| No JavaScript frameworks | Pure HTML + inline CSS, zero dependencies | -| API endpoint | `POST /analyze` — accepts `multipart/form-data` | - ---- - -## 6. Rule Engine - -The shared rule engine is the backbone of all 9 analyzers. - -### Rule Structure - -```yaml -- id: SEC001 - name: hardcoded_secrets - analyzer: secrets - severity: block # info | warning | critical | block - description: "Hardcoded secrets detected — credentials are exposed." - matcher: "secret_count > 0" - suggestion: "Use environment variables or a secrets vault." - attack_vector: "Attacker clones repo → extracts credentials → unauthorized access" - cwe: "CWE-798" -``` - -### Matcher DSL - -Two expression forms are supported: - -| Form | Example | -|------|---------| -| Simple comparison | `fact_name == value`, `fact_name > 0`, `fact_name != false` | -| Function call | `field_types('price') > 1`, `has_column('ssn') == true` | - -Operators: `==`, `!=`, `>`, `>=`, `<`, `<=`, `contains`, `startswith` - -### Rule Files - -| File | Analyzer | Rules | -|------|----------|-------| -| `rules/json_rules.yaml` | json | 8 | -| `rules/sql_rules.yaml` | sql | 8 | -| `rules/regex_rules.yaml` | regex | 7 | -| `rules/secrets_rules.yaml` | secrets | 5 | -| `rules/pii_rules.yaml` | pii | 4 | -| `rules/csv_rules.yaml` | csv | 5 | -| `rules/config_rules.yaml` | config | 9 | -| `rules/dockerfile_rules.yaml` | dockerfile | 8 | -| `rules/dependency_rules.yaml` | deps | 3 | -| **Total** | | **57 rules** | - ---- - -## 7. CI/CD Integration - -### GitHub Actions - -`.github/workflows/contractguard-ci.yml` runs on every push and pull request: - -- Installs ContractGuard -- Runs all 9 analyzers -- Uploads HTML report as artifact -- Uploads SARIF to GitHub Code Scanning -- Fails the build on BLOCK/CRITICAL findings (`--ci` flag) - -### Exit Codes - -| Code | Meaning | -|------|---------| -| `0` | Clean scan — no critical or block findings | -| `1` | Tool error (bad arguments, file not found) | -| `2` | Security findings above threshold (use with `--ci`) | - -### SARIF Upload to GitHub - -```yaml -- name: Upload SARIF - uses: github/codeql-action/upload-sarif@v2 - with: - sarif_file: results.sarif -``` - -### Pre-commit Hook - -`.pre-commit-config.yaml` runs secrets detection on every commit: - -```bash -pre-commit install -# Now every git commit scans staged files for secrets -``` - ---- - -## 8. Scan History & Trends - -ContractGuard tracks scan results over time in a local SQLite database (`.contractguard/history.db`). - -| Feature | Detail | -|---------|--------| -| Automatic storage | Use `--record` flag with any `analyze` command | -| Score tracking | Stores numeric score and grade per scan | -| Finding counts | Records block/critical/warning/info counts per scan | -| Trend analysis | Returns `improving`, `degrading`, or `stable` | -| History view | `contractguard history` — table of past scans with scores | -| Persistent | Survives across sessions — tracks project health over time | - ---- - -## 9. Severity Model - -ContractGuard uses a 4-tier severity model, stricter than most tools. - -| Severity | Weight | Meaning | CI Behavior | -|----------|--------|---------|-------------| -| `info` | 1 | Best practice, low risk | Pass | -| `warning` | 3 | Should fix before production | Pass | -| `critical` | 7 | Active security/reliability risk | Fail (`--ci`) | -| `block` | 15 | Deployment must stop immediately | Fail (`--ci`) | - -**BLOCK** is a custom severity above CRITICAL, reserved for findings that represent an immediate, exploitable risk: -- Hardcoded secrets (live credentials) -- SSN or credit card data in files -- Known CVSS 9.0+ CVEs in direct dependencies -- Insecure secret keys / default passwords - ---- - -## 10. Compliance Coverage - -| Regulation | Covered By | -|------------|-----------| -| **GDPR** (EU General Data Protection Regulation) | PII analyzer — email, DOB, IP, passport, IBAN | -| **CCPA** (California Consumer Privacy Act) | PII analyzer — SSN, credit cards, phone, driver's license | -| **HIPAA** (US Health Insurance Portability and Accountability Act) | PII analyzer — SSN, medical record numbers, health IDs | -| **PCI-DSS** (Payment Card Industry Data Security Standard) | PII analyzer — credit/debit card detection | -| **CWE** (Common Weakness Enumeration) | All analyzers — every rule carries a CWE ID | -| **OWASP Top 10** | SQL injection (SQL analyzer), secrets (secrets analyzer), vulnerable components (deps analyzer), security misconfiguration (config + dockerfile analyzers) | - ---- - -## Summary Stats - -| Metric | Value | -|--------|-------| -| Analyzer types | 9 | -| Total rules | 57+ | -| Secret patterns | 21 | -| PII patterns | 13 | -| Known CVE entries | 29 | -| Test coverage | 162 tests | -| Output formats | HTML, JSON, SARIF 2.1.0 | -| Offline operation | Yes — no API keys, no network | -| Python requirement | 3.11+ | +- No web upload UI +- No CSV analyzer +- No duplicate extension and CLI logic paths diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index ba3c5c2..6bdd161 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,274 +1,22 @@ -# ContractGuard — Deployment Guide +# Deployment Notes -> For usage instructions see [INSTRUCTIONS.md](INSTRUCTIONS.md). -> For capability reference see [CAPABILITIES.md](CAPABILITIES.md). +## Marketplace packaging ---- +The extension is packaged from the repository root with `vsce`. The package includes: -## Table of Contents +- `dist/` compiled extension entrypoint +- `src/contractguard/` Python engine +- `rules/` bundled rule files +- `media/` extension assets -1. [Local Development](#1-local-development) -2. [Docker](#2-docker) -3. [GitHub Actions CI](#3-github-actions-ci) -4. [Cloud Deployment — Web UI](#4-cloud-deployment--web-ui) -5. [Environment Variables](#5-environment-variables) -6. [Production Checklist](#6-production-checklist) +## Runtime model ---- +ContractGuard runs its analyzers out of process through `python -m contractguard.bridge`. The extension sets `PYTHONPATH` to its bundled `src/` directory so the engine can run without a separate package install step inside the extension host. -## 1. Local Development +## Publish checklist -### Setup - -```bash -git clone https://github.com/contractguard/contractguard.git -cd contractguard - -python -m venv .venv -source .venv/bin/activate # macOS/Linux -# .venv\Scripts\Activate.ps1 # Windows - -pip install -e ".[dev]" -``` - -### Run tests - -```bash -pytest tests/ -v -# 162 tests, ~2 seconds -``` - -### Run the CLI - -```bash -contractguard analyze --type all --path samples/ --score -``` - -### Run the web UI - -```bash -contractguard serve -# Listening at http://127.0.0.1:8000 -``` - ---- - -## 2. Docker - -### Dockerfile - -Create a `Dockerfile` in the project root: - -```dockerfile -FROM python:3.11-slim - -WORKDIR /app - -COPY pyproject.toml . -COPY src/ src/ -COPY rules/ rules/ - -RUN pip install --no-cache-dir -e . - -EXPOSE 8000 - -CMD ["contractguard", "serve", "--host", "0.0.0.0", "--port", "8000"] -``` - -> **Note:** The `serve` command needs `--host 0.0.0.0` to be accessible outside the container. Add that flag to `cli.py`'s `serve` command if deploying via Docker. - -### Build and run - -```bash -docker build -t contractguard:latest . -docker run -p 8000:8000 contractguard:latest -# Web UI at http://localhost:8000 -``` - -### Scan a local directory via Docker - -```bash -docker run --rm -v $(pwd)/myproject:/scan contractguard:latest \ - contractguard analyze --type all --path /scan --report-json /scan/report.json -``` - ---- - -## 3. GitHub Actions CI - -The workflow is already included at `.github/workflows/contractguard-ci.yml`. - -### What it does - -- Triggers on every `push` and `pull_request` -- Installs ContractGuard in a Python 3.11 environment -- Runs the full test suite (`pytest`) -- Runs `--type all` against the repo -- Uploads HTML report as a build artifact (downloadable from the Actions tab) -- Exports SARIF 2.1.0 and uploads to GitHub Code Scanning -- **Returns exit code 2** on CRITICAL or BLOCK findings — failing the build - -### Enable GitHub Code Scanning - -1. Push the workflow file to your repo -2. Go to **Settings → Code security and analysis → Code scanning** -3. After the first run, findings appear under **Security → Code scanning alerts** - -### Adding to an existing workflow - -```yaml -jobs: - security: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install ContractGuard - run: pip install contractguard - - - name: Run security scan - run: | - contractguard analyze --type all --path . \ - --ci \ - --report security-report.html \ - --report-sarif results.sarif - - - name: Upload HTML report - if: always() - uses: actions/upload-artifact@v4 - with: - name: security-report - path: security-report.html - - - name: Upload SARIF - if: always() - uses: github/codeql-action/upload-sarif@v2 - with: - sarif_file: results.sarif -``` - ---- - -## 4. Cloud Deployment — Web UI - -Deploy the FastAPI web UI to any Python-compatible cloud host. - -### Render (free tier) - -1. Push the repo to GitHub -2. Create a new **Web Service** on [render.com](https://render.com) -3. Set: - - **Build command:** `pip install -e .` - - **Start command:** `uvicorn contractguard.web:create_app --factory --host 0.0.0.0 --port $PORT` -4. Deploy — Render assigns a public URL - -### Railway - -1. Connect your GitHub repo on [railway.app](https://railway.app) -2. Set the start command: - ``` - uvicorn contractguard.web:create_app --factory --host 0.0.0.0 --port $PORT - ``` -3. Railway auto-detects Python and deploys - -### Heroku - -```bash -# Procfile -web: uvicorn contractguard.web:create_app --factory --host 0.0.0.0 --port $PORT -``` - -```bash -heroku create contractguard-app -git push heroku main -heroku open -``` - -### Fly.io - -```bash -fly launch -# Set start command to uvicorn as above -fly deploy -``` - -### Self-hosted VPS (nginx + systemd) - -1. Install Python 3.11 and clone the repo -2. Create a systemd service: - -```ini -# /etc/systemd/system/contractguard.service -[Unit] -Description=ContractGuard Web UI -After=network.target - -[Service] -WorkingDirectory=/opt/contractguard -ExecStart=/opt/contractguard/.venv/bin/uvicorn contractguard.web:create_app --factory --host 127.0.0.1 --port 8000 -Restart=always -User=www-data - -[Install] -WantedBy=multi-user.target -``` - -3. Configure nginx to proxy port 80 → 8000: - -```nginx -server { - listen 80; - server_name yourdomain.com; - - location / { - proxy_pass http://127.0.0.1:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } -} -``` - -```bash -sudo systemctl enable contractguard -sudo systemctl start contractguard -``` - ---- - -## 5. Environment Variables - -ContractGuard runs fully offline with no required environment variables. Optional configuration: - -| Variable | Default | Purpose | -|----------|---------|---------| -| `CONTRACTGUARD_RULES_DIR` | `./rules` | Path to custom rules directory | -| `CONTRACTGUARD_DB_PATH` | `.contractguard/history.db` | SQLite history database location | -| `PYTHONIOENCODING` | system default | Set to `utf-8` on Windows for emoji output | - -### Example `.env` for deployment - -```env -CONTRACTGUARD_RULES_DIR=/opt/contractguard/rules -CONTRACTGUARD_DB_PATH=/var/lib/contractguard/history.db -PYTHONIOENCODING=utf-8 -``` - ---- - -## 6. Production Checklist - -Before going live with the web UI: - -- [ ] Run behind a reverse proxy (nginx/Caddy) — never expose uvicorn directly on port 80/443 -- [ ] Enable HTTPS — use Let's Encrypt (Certbot) or your cloud provider's TLS termination -- [ ] Set file upload size limits in nginx (`client_max_body_size 10m`) -- [ ] Consider authentication if the instance is public-facing (the web UI has no auth by default) -- [ ] Set `PYTHONIOENCODING=utf-8` in the service environment on Linux hosts -- [ ] Store `history.db` on a persistent volume if deploying in containers -- [ ] Pin Docker base image to a specific digest, not `:latest` -- [ ] Run the container as a non-root user (add `USER nobody` to Dockerfile) -- [ ] Run `contractguard analyze --type all --path . --ci` in your deploy pipeline to gate releases +1. Build the extension with `tsc`. +2. Run the Python test suite. +3. Run `vsce package` and verify the generated VSIX. +4. Smoke test activation in VS Code using the packaged VSIX. +5. Publish under the `BlackplaneSystems` marketplace publisher with the matching marketplace token. diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index 9ee2376..c2363b1 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -1,428 +1,22 @@ -# ContractGuard — Step-by-Step Instructions +# ContractGuard Usage -> For a full capabilities reference see [CAPABILITIES.md](CAPABILITIES.md). -> For deployment options see [DEPLOYMENT.md](DEPLOYMENT.md). - ---- - -## Table of Contents - -1. [Installation](#1-installation) -2. [First Run (2 minutes)](#2-first-run-2-minutes) -3. [Running Each Analyzer](#3-running-each-analyzer) -4. [Reading the Results](#4-reading-the-results) -5. [Generating Reports](#5-generating-reports) -6. [Using the Web UI](#6-using-the-web-ui) -7. [Setting Up CI/CD](#7-setting-up-cicd) -8. [Scanning Your Own Project](#8-scanning-your-own-project) -9. [Pre-commit Hook Setup](#9-pre-commit-hook-setup) -10. [Troubleshooting](#10-troubleshooting) - ---- - -## 1. Installation - -### Requirements - -- Python 3.11 or higher -- pip - -### Windows +## CLI ```powershell -git clone https://github.com/contractguard/contractguard.git -cd contractguard -python -m venv .venv -.venv\Scripts\Activate.ps1 -pip install -e ".[dev]" -contractguard version -``` - -### macOS / Linux - -```bash -git clone https://github.com/contractguard/contractguard.git -cd contractguard -python -m venv .venv -source .venv/bin/activate -pip install -e ".[dev]" -contractguard version -``` - -### Verify Installation - -``` -$ contractguard version -ContractGuard v2.0.0 -``` - ---- - -## 2. First Run (2 minutes) - -### Scan everything in the included samples directory - -```bash -contractguard analyze --type all --path samples/ --score -``` - -Expected output: -- A findings table with 100+ issues across secrets, PII, config, Dockerfile, and dependencies -- A security grade panel showing **Grade F** with score breakdown -- Attack surface and top risks listed - -### Generate an HTML report - -```bash -contractguard analyze --type all --path samples/ --report my-first-report.html --score -``` - -Open `my-first-report.html` in your browser — dark theme, animated BLOCK badges, attack surface map. - ---- - -## 3. Running Each Analyzer - -### Secrets Detection - -Scans any file for hardcoded API keys, tokens, passwords, private keys. - -```bash -# Scan a single file -contractguard analyze --type secrets --path config/.env - -# Scan an entire directory -contractguard analyze --type secrets --path src/ - -# Fail CI pipeline if secrets found -contractguard analyze --type secrets --path . --ci -``` - -### PII Detection - -Scans for SSNs, credit cards, emails, medical records, passports. - -```bash -contractguard analyze --type pii --path data/ -contractguard analyze --type pii --path exports/customers.json -``` - -### Dependency Vulnerabilities - -Scans `requirements.txt` or `pyproject.toml` against a local CVE database. - -```bash -contractguard analyze --type deps --path requirements.txt -contractguard analyze --type deps --path . # finds requirements.txt automatically +.\.venv\Scripts\python.exe -m contractguard.cli analyze --type all --path . --score +.\.venv\Scripts\python.exe -m contractguard.cli analyze --type secrets --path . --report-sarif contractguard.sarif ``` -### Config Security - -Audits `.env`, `.yaml`, `.toml`, `.ini`, `.json` files for dangerous settings. - -```bash -contractguard analyze --type config --path config/ -contractguard analyze --type config --path .env -``` - -### Dockerfile - -Scans Dockerfiles for root execution, `:latest` tags, hardcoded secrets, `curl | bash`. - -```bash -contractguard analyze --type dockerfile --path . -contractguard analyze --type dockerfile --path docker/Dockerfile.prod -``` - -### JSON Schema - -Detects type drift, inconsistent schemas, nullable fields. - -```bash -contractguard analyze --type json --path api/payloads/ -contractguard analyze --type json --path data.json -``` - -### SQL Analysis - -Flags `SELECT *`, missing `WHERE`, cartesian joins, ReDoS-prone `LIKE`. - -```bash -contractguard analyze --type sql --path queries/ -contractguard analyze --type sql --path migrations/ -``` - -### Regex Complexity - -Detects ReDoS-vulnerable patterns, nested quantifiers, excessive complexity. - -```bash -contractguard analyze --type regex --path src/validators.py -contractguard analyze --type regex --path patterns/ -``` - -### CSV Data Quality - -Checks type consistency, null rates, duplicate rows, encoding issues. - -```bash -contractguard analyze --type csv --path data/exports/ -``` - -### Full Scan (All Analyzers) - -```bash -contractguard analyze --type all --path . --score --report report.html -``` - ---- - -## 4. Reading the Results - -### CLI Output - -Each finding shows: - -| Column | Meaning | -|--------|---------| -| ID | Rule ID (e.g. `SEC001`, `DEP002`) | -| Severity | `INFO` / `WARNING` / `CRITICAL` / `🚫 BLOCK` | -| Description | What was found | -| Location | File path | -| Suggestion | How to fix it | - -### Severity at a Glance - -- `INFO` — awareness only, no action required -- `WARNING` — fix before your next release -- `CRITICAL` — active risk, fix before any deployment -- `🚫 BLOCK` — stop everything, fix immediately - -### Score Panel (with `--score`) - -``` -╭─ Security Score ──────────────────────────╮ -│ Grade: F | Score: 0/100 │ -│ BLOCK: 30 CRITICAL: 130 WARNING: 4 │ -│ Attack Surface: credential_theft, ... │ -│ Top Risks: hardcoded_secrets, ... │ -╰───────────────────────────────────────────╯ -``` - ---- - -## 5. Generating Reports - -### HTML Report - -```bash -contractguard analyze --type all --path . --report security-report.html -``` - -Open in any browser. Includes grade banner, attack surface map, animated BLOCK badges. - -### JSON Report (machine-readable) - -```bash -contractguard analyze --type all --path . --report-json findings.json -``` - -Pipe into other tools, dashboards, or custom scripts. - -### SARIF 2.1.0 (GitHub Code Scanning) - -```bash -contractguard analyze --type all --path . --report-sarif results.sarif -``` - -Upload to GitHub Security tab for inline annotations on your code. - -### All three at once - -```bash -contractguard analyze --type all --path . \ - --report report.html \ - --report-json report.json \ - --report-sarif report.sarif \ - --score -``` - ---- - -## 6. Using the Web UI - -### Start the server - -```bash -contractguard serve -``` - -Opens at `http://127.0.0.1:8000` - -### Usage - -1. Open `http://127.0.0.1:8000` in your browser -2. Choose an analyzer type from the dropdown (or select **all**) -3. Click **Choose File** and upload the file you want to scan -4. Click **Analyze** -5. Results appear inline on the page - -### What you can upload - -- Any text file for secrets/PII scanning -- `.env`, `.yaml`, `.toml`, `.json`, `.ini` for config scanning -- `Dockerfile` for container analysis -- `requirements.txt` for dependency scanning -- `.csv` files for data quality analysis -- SQL files for query analysis -- Any `.json` file for schema analysis - ---- - -## 7. Setting Up CI/CD - -### GitHub Actions (built-in) - -The workflow file `.github/workflows/contractguard-ci.yml` is already included. - -It runs on every `push` and `pull_request`: -- Scans the whole repo with `--type all` -- Uploads HTML report as a downloadable artifact -- Uploads SARIF to GitHub Code Scanning -- **Fails the build** (`exit 2`) on CRITICAL or BLOCK findings - -Activate it by pushing to GitHub — no additional setup needed. - -### Manual CI integration - -Add this to any CI pipeline: - -```bash -pip install contractguard -contractguard analyze --type all --path . --ci --report-sarif results.sarif -# exit code 2 = critical/block findings found -``` - -### GitHub SARIF Upload (in your existing workflow) - -```yaml -- name: Run ContractGuard - run: contractguard analyze --type all --path . --report-sarif results.sarif - -- name: Upload SARIF - uses: github/codeql-action/upload-sarif@v2 - with: - sarif_file: results.sarif -``` - -### Track scan history in CI - -```bash -contractguard analyze --type all --path . --ci --record -contractguard history -``` - ---- - -## 8. Scanning Your Own Project - -ContractGuard can analyze its own source code (and any Python project): - -```bash -# Secrets and PII in source files -contractguard analyze --type secrets --path src/ -contractguard analyze --type pii --path src/ - -# Dependency vulnerabilities -contractguard analyze --type deps --path pyproject.toml - -# Config files -contractguard analyze --type config --path . - -# Full project scan -contractguard analyze --type all --path . --score -``` - -> **Note:** The tool flags its own `secrets_analyzer.py` for containing PEM header strings (e.g. `-----BEGIN PRIVATE KEY-----`). These are regex detection patterns, not real secrets — but this demonstrates how aggressive the scanner is. In production, add a `.contractguardignore` exclusion list (future feature) or note these as false positives. - -### Scan history and trends - -```bash -# Record today's scan -contractguard analyze --type all --path . --score --record - -# Check trend over time -contractguard history -``` - ---- - -## 9. Pre-commit Hook Setup - -Run secrets detection automatically on every `git commit`. - -### Install - -```bash -pip install pre-commit -pre-commit install -``` - -### What it does - -Every `git commit` will run: -- Secrets scan on all staged files -- If secrets are found, the commit is **blocked** with a finding report - -### Manual run (all files) - -```bash -pre-commit run --all-files -``` - ---- - -## 10. Troubleshooting - -### `UnicodeEncodeError` on Windows - -Set UTF-8 encoding before running: +## Bridge ```powershell -$env:PYTHONIOENCODING = "utf-8" -contractguard analyze ... -``` - -Or add it to your PowerShell profile permanently. - -### `contractguard: command not found` - -Make sure your virtual environment is activated: - -```bash -# Windows -.venv\Scripts\Activate.ps1 - -# macOS/Linux -source .venv/bin/activate +$env:PYTHONPATH = (Resolve-Path .\src).Path +.\.venv\Scripts\python.exe -m contractguard.bridge scan --path . --analyzer all --include-sarif ``` -Then reinstall: `pip install -e .` - -### No findings returned - -- Check that the path points to real files: `--path src/` not `--path src` -- Make sure the file type matches the analyzer (e.g. `--type deps` needs a `requirements.txt`) -- Try `--type all` for broad coverage - -### Web UI returns 500 error - -Check the terminal running `contractguard serve` for the traceback. Common cause: uploaded file has binary content when the analyzer expects text. - -### Tests failing after code changes - -```bash -pytest tests/ -v --tb=short -``` +## VS Code -Check the specific failing test for the assertion that broke. +1. Build the extension with `tsc`. +2. Install the generated VSIX. +3. Run `ContractGuard: Install Python Runtime Dependencies` if the runtime is missing. +4. Use `ContractGuard: Scan Workspace` or enable scan-on-save. diff --git a/README.md b/README.md index bba0035..742b42d 100644 --- a/README.md +++ b/README.md @@ -1,248 +1,75 @@ -# ContractGuard +# ContractGuard for VS Code -**Stop bad inputs before they break your systems.** +ContractGuard is a VS Code extension backed by a Python security analysis core. It scans source trees for schema drift, risky SQL, regex complexity, secrets, PII, insecure configuration, Dockerfile issues, and vulnerable dependencies, then surfaces the results as diagnostics, a findings explorer, a status bar score, and SARIF exports. -ContractGuard is a production-grade security analysis platform that scans your entire project — data files, configs, Dockerfiles, dependencies, source code — and flags reliability, safety, and security issues **before they reach production**. Think of it as a security-first linter for everything that touches your pipeline. +## What ships in this repository -**9 Analyzers. 1 Engine. 57+ Rules. A-F Security Grading. SARIF Export. CI/CD Ready.** +- A reusable Python engine in `src/contractguard` with rule-driven analyzers, scoring, findings, history, and SARIF generation. +- A VS Code extension in `vscode-src` that runs the engine in a separate Python process and renders results inside the editor. +- Rules in `rules/` that stay bundled with the extension and CLI. ---- +## Supported analyzers -## What It Catches +- JSON schema analysis +- SQL analysis +- Regex complexity analysis +- Secrets detection +- PII detection +- Config security analysis +- Dockerfile linting +- Dependency vulnerability analysis -| Analyzer | What It Detects | Severity Range | -|----------|-----------------|----------------| -| **JSON Schema** | Type drift, inconsistent schemas, nullable fields | Warning-Critical | -| **SQL Performance** | `SELECT *`, missing `WHERE`, cartesian joins, ReDoS-prone LIKE | Warning-Critical | -| **Regex Complexity** | Nested quantifiers (ReDoS), backreferences, catastrophic backtracking | Warning-Critical | -| **Secrets Detection** | AWS keys, GitHub tokens, Stripe keys, private keys, JWTs, DB URLs (21 patterns) | Critical-BLOCK | -| **PII Detection** | SSNs, credit cards, emails, phone numbers, medical records, passports, IBAN | Critical-BLOCK | -| **CSV Data Quality** | Mixed types, null-heavy columns, duplicate rows, encoding issues | Info-Warning | -| **Config Security** | Debug mode, CORS wildcards, weak secrets, SSL disabled, default passwords | Warning-Critical | -| **Dockerfile Lint** | Running as root, `:latest` tags, `COPY .`, hardcoded secrets, `curl\|bash` | Warning-Critical | -| **Dependency Vulns** | 29+ known CVEs (Django, Flask, cryptography, urllib3, etc.) — offline DB | Warning-BLOCK | +## VS Code features -All analyzers share a **YAML-based rule engine** with consistent severities, CWE IDs, attack vector descriptions, and actionable suggestions. +- `ContractGuard: Scan Workspace` +- `ContractGuard: Scan Current File` +- `ContractGuard: Export SARIF` +- `ContractGuard: Clear Findings` +- Findings tree view grouped by severity +- Inline diagnostics and quick navigation +- Status bar security grade +- Debounced scan-on-save +- Configurable analyzer set and disabled rules ---- +## Runtime requirements -## Quick Start +- Python 3.11+ available on the machine running VS Code +- Python packages from `python-requirements.txt` -```bash -# Clone and install -git clone https://github.com/contractguard/contractguard.git -cd contractguard -python -m venv .venv -.venv\Scripts\activate # Windows -# source .venv/bin/activate # macOS/Linux -pip install -e ".[dev]" -``` - -### Run a Full Scan - -```bash -# Scan EVERYTHING — all 9 analyzers at once -contractguard analyze --type all --path . --report security-report.html --score - -# Individual analyzers -contractguard analyze --type secrets --path . -contractguard analyze --type deps --path requirements.txt -contractguard analyze --type dockerfile --path . -contractguard analyze --type config --path config/ -contractguard analyze --type pii --path data/ -contractguard analyze --type sql --path queries/ --ci -``` - -### Security Grade - -```bash -contractguard score --path . -# Output: Grade F | Score 0/100 | 30 BLOCK, 130 CRITICAL findings -``` - -### Scan History & Trends - -```bash -contractguard analyze --type all --path . --record # Track over time -contractguard history # Show trend -``` - -### Watch Mode - -```bash -contractguard watch --path src/ --type secrets --interval 5 -# Re-scans on every file change -``` - -### SARIF Export (GitHub Code Scanning) - -```bash -contractguard analyze --type all --path . --report-sarif results.sarif -# Upload to GitHub → Security tab → Code scanning alerts -``` - -### Web UI - -```bash -contractguard serve -# Open http://127.0.0.1:8000 — upload files, pick analyzer, get instant report -``` - -### CI/CD Integration - -```bash -# In your CI pipeline — fails on critical/block findings -contractguard analyze --type all --path . --ci --report-sarif results.sarif -``` - ---- +For local development in this repository: -## 90-Second Demo Script - -> **Step 1 — The Problem (10s)** -> "Your API schemas drift. Your SQL hides performance bombs. Your configs leak secrets. Your Dockerfiles run as root. Your dependencies have known CVEs." - -> **Step 2 — Full Project Scan (20s)** -> ```bash -> contractguard analyze --type all --path samples/ --report report.html --score -> ``` -> Open report.html — Grade F, 164 findings, attack surface mapped, BLOCK-level secrets and vulnerabilities flagged. - -> **Step 3 — Secrets Detection (15s)** -> ```bash -> contractguard analyze --type secrets --path samples/secrets/ -> ``` -> AWS keys detected, DB passwords found, private keys exposed — each with CWE IDs and attack scenarios. - -> **Step 4 — Dependency Vulnerabilities (15s)** -> ```bash -> contractguard analyze --type deps --path samples/deps/ -> ``` -> Django CVE-2023-46695, cryptography NULL-ptr deref, Werkzeug DoS — all caught offline, no API needed. - -> **Step 5 — SARIF + CI (15s)** -> "Upload the SARIF file to GitHub Security tab. In CI, BLOCK-level findings return exit code 2 — deployment stops automatically." - -> **Step 6 — Web UI (15s)** -> Start the web server, upload a Dockerfile, see instant results with attack vectors and remediation steps. - ---- - -## Architecture - -``` -contractguard/ -├── src/contractguard/ -│ ├── __init__.py # v2.0.0 -│ ├── cli.py # Typer CLI — analyze, score, history, watch, serve -│ ├── engine.py # YAML rule engine — BLOCK severity, CWE, attack vectors -│ ├── reporter.py # HTML (dark theme + grade), JSON, SARIF 2.1.0 -│ ├── scorer.py # A-F security grade calculator -│ ├── history.py # SQLite scan tracking + trend analysis -│ ├── web.py # FastAPI web UI (all 9 analyzers) -│ └── analyzers/ -│ ├── json_analyzer.py # JSON schema inference -│ ├── sql_analyzer.py # SQL static analysis -│ ├── regex_analyzer.py # Regex complexity / ReDoS detection -│ ├── secrets_analyzer.py # 21+ secret patterns (AWS, GCP, Stripe, etc.) -│ ├── pii_analyzer.py # 13 PII patterns (SSN, CC, IBAN, MRN, etc.) -│ ├── csv_analyzer.py # CSV type/null/duplicate detection -│ ├── config_analyzer.py # Config security audit (YAML/TOML/ENV) -│ ├── dockerfile_analyzer.py # Dockerfile security linting -│ └── dependency_analyzer.py # Offline CVE scanner (29+ vulns) -├── rules/ # 9 YAML rule files, 57+ rules -├── samples/ # Demo inputs for all analyzer types -├── tests/ # 162 unit tests -├── .github/workflows/ # GitHub Actions CI -└── pyproject.toml +```powershell +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install -r python-requirements.txt ``` ---- - -## Severity Levels - -| Level | Meaning | CI Behavior | -|-------|---------|-------------| -| `info` | Best practice suggestion | Pass | -| `warning` | Should fix before production | Pass | -| `critical` | Security/reliability risk | Fail (`--ci`) | -| `block` | Deployment must be stopped immediately | Fail (`--ci`) | +## Development commands ---- +Python: -## Rule Format - -```yaml -- id: SEC001 - name: hardcoded_secrets - analyzer: secrets - severity: block - description: "Hardcoded secrets found — immediate rotation required." - matcher: "secret_count > 0" - suggestion: "Use environment variables or a secrets vault (AWS SSM, HashiCorp Vault)." - attack_vector: "Attacker clones repo → extracts credentials → gains unauthorized access" - cwe: "CWE-798" +```powershell +.\.venv\Scripts\python.exe -m pytest +.\.venv\Scripts\python.exe -m contractguard.bridge scan --path . --analyzer all --include-sarif ``` ---- - -## CLI Reference +Extension: +```powershell +node .\node_modules\typescript\bin\tsc -p .\tsconfig.json +node .\node_modules\@vscode\vsce\vsce package ``` -contractguard analyze [OPTIONS] - -t, --type TEXT json|sql|regex|secrets|pii|csv|config|dockerfile|deps|all - -p, --path PATH File or directory to scan - --report PATH HTML report output - --report-json PATH JSON report output - --report-sarif PATH SARIF 2.1.0 output (GitHub Code Scanning) - --score Show security grade after scan - --record Save to history database - --ci Exit 2 on critical/block findings - -contractguard score --path . # Full scan → letter grade -contractguard history # Scan trends -contractguard watch --path . --type all # Re-scan on changes -contractguard serve # Web UI on :8000 -``` - ---- - -## Hackathon Themes - -- **Software Development & Engineering** — CLI tool, CI/CD integration, rule-based architecture -- **Cybersecurity & Privacy** — Secrets detection, PII scanning, Dockerfile hardening, dependency CVEs, SARIF -- **Data Science & Analytics** — Schema inference, CSV quality analysis, data contract enforcement -- **Business & Productivity** — Security scoring, trend tracking, automated compliance checks - ---- - -## Future Scope (VS Code Extension Ready) - -ContractGuard is architecturally ready for VS Code extension conversion: - -- **Language Server Protocol**: Each analyzer returns structured `Finding` objects with file paths and line numbers — directly mappable to VS Code Diagnostics -- **Real-time analysis**: The `watch` mode already implements file-change detection — LSP `onDidChangeTextDocument` is a natural fit -- **Inline annotations**: Every finding includes severity, description, suggestion, CWE, and attack vector — all displayable as hover tooltips -- **Quick fixes**: Suggestions can power VS Code Quick Fix actions (e.g., "Remove hardcoded secret", "Pin Docker image tag") -- **Security Score in status bar**: The scoring system maps directly to a status bar item showing project health -- **SARIF integration**: VS Code's SARIF Viewer extension can display ContractGuard output natively - ---- - -## Tech Stack -| Component | Library | -|-----------|---------| -| CLI | Typer + Rich | -| Rule Engine | PyYAML + custom evaluator | -| HTML Reports | Jinja2 (dark theme, security grade, attack vectors) | -| SARIF | Custom 2.1.0 generator | -| Web UI | FastAPI + Uvicorn | -| History | SQLite3 | -| Testing | pytest (162 tests) | +## Settings ---- +- `contractguard.pythonPath` +- `contractguard.scanOnSave` +- `contractguard.scanDebounceMs` +- `contractguard.enabledAnalyzers` +- `contractguard.disabledRules` +- `contractguard.rulesDirectory` +- `contractguard.sqlExplainDatabase` -## License +## Packaging -MIT +The extension is packaged from the repository root. The VSIX includes the compiled extension, bundled Python source, rules, and documentation. The output artifact is written to `dist-vsix/`. diff --git a/media/icon.png b/media/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e82ac9be8a963f7218bccc7804c7ace30e7c75bf GIT binary patch literal 1385 zcmcgseN>WX6o21WV0@Whh(@hbNjV%0%r!iRO#Di){5Vt-EfZGDMjztQCZ!qZw3Rl? zCRUQ!G;3*!;Q}Yq7tm@#)^?iZ(mwEIxB6#iXMgXHd++(3=ehT}zx&+t zJDidf?@6Q)0RZq63SvcQRK7Mm4)vDRQY#w3Tv2>9&^oa2F$yqQQHfCi&|XP$OR*>> z$OUP+05H$;+Q94C7q_8zUI}BPHYm1C{@itw&h?HPP2~Huuw$;S>TBXt?luMSp`%!; zJ#u6sptdq(EIcfO{^WscQa^prUDG>W8%b{$fW;>uOfVLoMD*5B7KbzVa*@)}2$+Qb zJU}4#pJR4*Pu$W*;QUjT`}4ig1j$fAcvpy(Bxz4YY}H~BKq~Z|K7Hm>jP@eUe(CBoj(z^F;Yx04Q?G!ux6!D(LLKmwCfVnHUz#4bryHd^CdLd`ZJacub{ODjqoYf}6iD zJ2!BoV7GM3<@qAOvWbxI?~>i*K!F=Dw#AJ=nXjjd(>B-$oC)=E$r6dT=l{=;`@(fS zG83tduvv`hC!k^2*(Q)Idicv}ieoADgQA|&eJ4|Qij*YrvtZ4Td6lhsB?A^R_mh6V z%6ZeU99sLpGQ@hDt%0lb@;zQ}DZJDE8%xtHeOn7q8%uNJ-=yS&Yii)C=kjOx?dVYU zWhjf+F6e(xj4T>h)W@`4S{Z}qe1oAH59#7VOE?f^b0M9;RCr}oI;SG~*?E5T5}@pk z2MK*N%+NJt>`Q?($Ke%I)LFk_cG2 zx?kbz+Dmh@a_%A!h1|GYb-vJiDnc`2?$jB!PS28J@F|=?%OC#bL*~xpaNQwYR!)4_ zBa;@I5K2M|!tXmZL55941oV(GRQ8F{3IAd1NI~dj09*d+*x}A*!+N5&Br1ZX>X{tP zNR`(d?`M;P`6igWk-4W_XJ4)AGVIK5@~&U58XxI%wK?Z8U;=rHMOgJbr29bcEXj&b z(c0cuJlt4VK$L?XyI88Mj6QBid9`=zEYh0$vA3&Rvfkdh_jfEuHKlHN_Zl^=j2_O!RW=;x?R=w?771U7$^F&IO z_T+_S$b^4Cn;FD66*Fh%)lH7*u5)dcjr z%nj;HJcDm?2B|Xzph(#kxn{}!7p0WN*twTm=}T)e@oawHf9TM^1j-eObA9_b^mO9_ PR2~6gTvBXnw50fN*o|PX literal 0 HcmV?d00001 diff --git a/media/icon.svg b/media/icon.svg new file mode 100644 index 0000000..e507ff2 --- /dev/null +++ b/media/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..46574dd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4001 @@ +{ + "name": "contractguard", + "version": "3.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "contractguard", + "version": "3.0.0", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^20.16.1", + "@types/vscode": "^1.92.0", + "@vscode/vsce": "^3.2.2", + "typescript": "^5.6.2" + }, + "engines": { + "vscode": "^1.92.0" + } + }, + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "@azu/format-text": "^1.0.1" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.10.0.tgz", + "integrity": "sha512-2Y4TlG5mCfxviHutfW50i8Xd8xhGKTgieL02vMYOE5ZbZrVM+drKSGD//tweRAmlmqqp+F9vrKoHWri/buzxWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.6.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.0.tgz", + "integrity": "sha512-FemGljX0csPlBMUE5GUan7BfRn1emeMRUhHSARhqzLN6LA9nt+MgzmAQ1xVqdLm+6plVoxsq9mS5eoyKtpPSgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.0.tgz", + "integrity": "sha512-b/ak8XAqpnGk1N1nsyTVV0Remp48BP3QrGQZ1uCMcvg2S8X1eSXzhHQZEae2oX276Q4KFAqCUswanDtcvIKLrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.6.0", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-sarif-builder": "^3.2.0" + } + }, + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@textlint/ast-node-types": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.6.0.tgz", + "integrity": "sha512-CxZHFbYAU7J0A4izz31wV2ZZfySR6aVj2OSR6/3tppZm7VV6hM7nA7sutsLwIiBL/v4lsB1RM79l4Dc/VrH4qw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.6.0.tgz", + "integrity": "sha512-IwHRhjwxs0a5t1eNAoKAdV224CDca38LyopPofXpwO/d0J75wBvzf/cBHXNl4TMsLKhYGtR83UprcLEKj/gZsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.6.0", + "@textlint/resolver": "15.6.0", + "@textlint/types": "15.6.0", + "chalk": "^4.1.2", + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "lodash": "^4.18.1", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/module-interop": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.6.0.tgz", + "integrity": "sha512-MHY6pJx9i5kOlrvUSK51887tYZjHAV2qnr6unBm7LtBLGDFo93utdYqHyWep8r9QLsilQdeijWtufJI46z4v4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/resolver": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.6.0.tgz", + "integrity": "sha512-T1l2Gd3455pwtm0cTewhX/LLy3bL9z6/Fu/am+jj+jjGfXVoknYkjfkZEKrjHlA7xzay0EfUKnu//teYemLeZw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/types": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.6.0.tgz", + "integrity": "sha512-CvgYb1PiqF4BGyoZebGWzAJCZ4ChJAZ9gtWjpQIMKE4Xe2KlSwDA8m8MsiZIV321f5Ibx38BMjC1Z/2ZYP2GQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@textlint/ast-node-types": "15.6.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.40", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.40.tgz", + "integrity": "sha512-xxx6M2IpSTnnKcR0cMvIiohkiCx20/oRPtWGbenFygKCGl3zqUzdNjQ/1V4solq1LU+dgv0nQzeGOuqkqZGg0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.118.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.118.0.tgz", + "integrity": "sha512-Ah6eTlqDcwIMELEVwQMO++rJAFBRz/oLluLD/vWdYrH1KuI9kfpaM+7pg0OvvascgcJy+ghLCERAYouM4QbzGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@vscode/vsce": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.1.tgz", + "integrity": "sha512-MPn5p+DoudI+3GfJSpAZZraE1lgLv0LcwbH3+xy7RgEhty3UIkmUMUA+5jPTDaxXae00AnX5u77FxGM8FhfKKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@secretlint/node": "^10.1.2", + "@secretlint/secretlint-formatter-sarif": "^10.1.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^11.0.0", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "secretlint": "^10.1.2", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^3.2.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", + "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-sarif-builder": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc-config-loader": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", + "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "json5": "^2.2.3", + "require-from-string": "^2.0.2" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/secretlint": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-creator": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/node": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "debug": "^4.4.1", + "globby": "^14.1.0", + "read-pkg": "^9.0.1" + }, + "bin": { + "secretlint": "bin/secretlint.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz", + "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ee303e2 --- /dev/null +++ b/package.json @@ -0,0 +1,170 @@ +{ + "name": "contractguard", + "displayName": "ContractGuard", + "description": "Security analysis for code, config, Dockerfiles, data payloads, and dependencies.", + "version": "3.0.0", + "publisher": "BlackplaneSystems", + "license": "Apache-2.0", + "icon": "media/icon.png", + "repository": { + "type": "git", + "url": "https://github.com/Blackplane-Systems/contractguard" + }, + "homepage": "https://github.com/Blackplane-Systems/contractguard", + "engines": { + "vscode": "^1.92.0" + }, + "categories": [ + "Linters", + "Programming Languages", + "Other" + ], + "keywords": [ + "security", + "sarif", + "sast", + "secrets", + "sql", + "dockerfile", + "regex", + "pii" + ], + "activationEvents": [ + "onStartupFinished", + "onCommand:contractguard.scanWorkspace", + "onCommand:contractguard.scanCurrentFile", + "onView:contractguard.findings" + ], + "main": "./dist/extension.js", + "files": [ + "dist/**", + "src/contractguard/**", + "rules/**", + "media/**", + "python-requirements.txt", + "README.md", + "LICENSE" + ], + "contributes": { + "commands": [ + { + "command": "contractguard.scanWorkspace", + "title": "ContractGuard: Scan Workspace" + }, + { + "command": "contractguard.scanCurrentFile", + "title": "ContractGuard: Scan Current File" + }, + { + "command": "contractguard.exportSarif", + "title": "ContractGuard: Export SARIF" + }, + { + "command": "contractguard.clearFindings", + "title": "ContractGuard: Clear Findings" + }, + { + "command": "contractguard.openFinding", + "title": "ContractGuard: Open Finding" + }, + { + "command": "contractguard.installRuntime", + "title": "ContractGuard: Install Python Runtime Dependencies" + } + ], + "views": { + "explorer": [ + { + "id": "contractguard.findings", + "name": "ContractGuard Findings" + } + ] + }, + "menus": { + "view/title": [ + { + "command": "contractguard.scanWorkspace", + "when": "view == contractguard.findings", + "group": "navigation" + }, + { + "command": "contractguard.exportSarif", + "when": "view == contractguard.findings", + "group": "navigation" + }, + { + "command": "contractguard.clearFindings", + "when": "view == contractguard.findings", + "group": "navigation" + } + ] + }, + "configuration": { + "title": "ContractGuard", + "properties": { + "contractguard.pythonPath": { + "type": "string", + "default": "", + "description": "Explicit Python interpreter path used to run the ContractGuard bridge." + }, + "contractguard.scanOnSave": { + "type": "boolean", + "default": true, + "description": "Run ContractGuard after saving a supported file." + }, + "contractguard.scanDebounceMs": { + "type": "number", + "default": 600, + "minimum": 0, + "description": "Debounce delay for automatic scans." + }, + "contractguard.enabledAnalyzers": { + "type": "array", + "default": [ + "json", + "sql", + "regex", + "secrets", + "pii", + "config", + "dockerfile", + "deps" + ], + "items": { + "type": "string" + }, + "description": "Subset of analyzers used by the extension when scanning." + }, + "contractguard.disabledRules": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "Rule ids hidden in the VS Code client." + }, + "contractguard.rulesDirectory": { + "type": "string", + "default": "", + "description": "Override rules directory. When empty, bundled rules are used." + }, + "contractguard.sqlExplainDatabase": { + "type": "string", + "default": "", + "description": "Optional SQLite database path for SQL EXPLAIN mode." + } + } + } + }, + "scripts": { + "build": "tsc -p ./tsconfig.json", + "package": "vsce package --out dist-vsix/contractguard-3.0.0.vsix", + "prepackage": "npm run build" + }, + "devDependencies": { + "@types/node": "^20.16.1", + "@types/vscode": "^1.92.0", + "@vscode/vsce": "^3.2.2", + "typescript": "^5.6.2" + } +} diff --git a/pyproject.toml b/pyproject.toml index f78d2a7..4458b61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "contractguard" -version = "2.0.0" -description = "Stop bad inputs before they break your systems — 9 analyzers for secrets, PII, SQL, configs, Dockerfiles, dependencies, and more." +version = "3.0.0" +description = "ContractGuard security analysis core for VS Code and CI workflows." readme = "README.md" license = {text = "Apache-2.0"} requires-python = ">=3.11" @@ -26,9 +26,6 @@ dependencies = [ "jsonschema>=4.20.0", "sqlparse>=0.5.0", "jinja2>=3.1.0", - "fastapi>=0.104.0", - "uvicorn>=0.24.0", - "python-multipart>=0.0.6", ] [project.optional-dependencies] @@ -40,9 +37,10 @@ dev = [ [project.scripts] contractguard = "contractguard.cli:app" +contractguard-bridge = "contractguard.bridge:app" [project.urls] -Homepage = "https://github.com/contractguard/contractguard" +Homepage = "https://github.com/Blackplane-Systems/contractguard" [tool.setuptools.packages.find] where = ["src"] diff --git a/python-requirements.txt b/python-requirements.txt new file mode 100644 index 0000000..e34cde3 --- /dev/null +++ b/python-requirements.txt @@ -0,0 +1,9 @@ +typer>=0.9.0 +rich>=13.0.0 +pyyaml>=6.0 +jsonschema>=4.20.0 +sqlparse>=0.5.0 +jinja2>=3.1.0 +pytest>=7.4.0 +pytest-cov>=4.1.0 +httpx>=0.25.0 diff --git a/rules/csv_rules.yaml b/rules/csv_rules.yaml deleted file mode 100644 index 887c025..0000000 --- a/rules/csv_rules.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# ContractGuard — CSV Data Quality Rules - -- id: CSV001 - name: mixed_type_columns - analyzer: csv - severity: warning - description: "CSV columns have mixed data types — will cause ETL pipeline failures." - matcher: "mixed_type_columns > 0" - suggestion: "Enforce column types at ingestion. Add schema validation to your data pipeline." - cwe: "CWE-20" - -- id: CSV002 - name: inconsistent_column_count - analyzer: csv - severity: critical - description: "Rows have different column counts — corrupted or malformed CSV." - matcher: "inconsistent_column_count == true" - suggestion: "Fix the source data export. Add column count validation to your ingestion pipeline." - -- id: CSV003 - name: high_null_rate - analyzer: csv - severity: warning - description: "Columns with >50% null values detected — data quality issue." - matcher: "null_heavy_columns > 0" - suggestion: "Investigate why data is missing. Set default values or remove unused columns." - -- id: CSV004 - name: duplicate_rows - analyzer: csv - severity: warning - description: "Duplicate rows detected — may indicate ETL bugs or missing deduplication." - matcher: "duplicate_rows > 0" - suggestion: "Add deduplication to your pipeline. Check for missing primary key constraints." - -- id: CSV005 - name: encoding_issues - analyzer: csv - severity: critical - description: "File encoding issues detected — data corruption risk." - matcher: "has_encoding_issues == true" - suggestion: "Standardize on UTF-8 encoding. Fix the data export to use consistent encoding." diff --git a/samples/config/dangerous.env b/samples/config/dangerous.env index 6d1c749..6e08bc5 100644 --- a/samples/config/dangerous.env +++ b/samples/config/dangerous.env @@ -18,11 +18,9 @@ ALLOWED_HOSTS=* # CORS — wide open CORS_ALLOW_ALL_ORIGINS=True -# API keys (leaked!) -AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE -AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY -STRIPE_SECRET_KEY=sk_live_51J0EXAMPLE -GITHUB_TOKEN=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh +# Placeholder credentials +INTERNAL_API_SECRET=demo-config-secret +PAYMENTS_TOKEN=demo-payments-token # Logging SHOW_ERRORS=true diff --git a/samples/csv/users.csv b/samples/csv/users.csv deleted file mode 100644 index 6a84633..0000000 --- a/samples/csv/users.csv +++ /dev/null @@ -1,16 +0,0 @@ -id,name,email,phone,ssn,credit_card,dob,salary,department,notes -1,John Doe,john.doe@example.com,555-123-4567,123-45-6789,4111111111111111,1990-01-15,85000,Engineering, -2,Jane Smith,jane.smith@example.com,555-234-5678,234-56-7890,5500000000000004,1985-06-22,92000,Marketing,Great performer -3,Bob Wilson,bob@test.com,,345-67-8901,,1992-03-10,78000,Sales, -4,Alice Brown,alice.b@example.com,555-456-7890,456-78-9012,4111111111111111,1988-11-30,,Engineering, -5,Charlie Davis,charlie@example.com,555-567-8901,,378282246310005,1995-07-04,67000,Support,New hire -6,Diana Miller,diana@example.com,555-678-9012,567-89-0123,6011111111111117,,88000,Engineering, -7,Edward Jones,edward.j@example.com,,678-90-1234,5105105105105100,1991-09-18,95000,Management, -8,Frank Garcia,frank@example.com,555-890-1234,789-01-2345,,1987-12-25,72000,Sales,Top performer -9,Grace Lee,,555-901-2345,890-12-3456,4012888888881881,1993-04-14,81000,Engineering,Remote -10,Henry Wang,henry@example.com,555-012-3456,,2223003122003222,1989-08-08,missing,Marketing, -11,Iris Taylor,iris@example.com,555-123-ABCD,901-23-4567,4111111111111111,1994-02-28,76000,Support, -12,Jack Brown,jack.b@example.com,555-234-5678,012-34-5678,5500000000000004,01/15/1990,83000,Engineering, -13,John Doe,john.doe@example.com,555-123-4567,123-45-6789,4111111111111111,1990-01-15,85000,Engineering, -14,,,,,,,, -15,Kate Wilson,kate@example.com,555-345-6789,invalid-ssn,0000000000000000,1996-10-10,71000,Sales,Half-time diff --git a/samples/secrets/leaked.env b/samples/secrets/leaked.env index cc4b682..be4819f 100644 --- a/samples/secrets/leaked.env +++ b/samples/secrets/leaked.env @@ -1,38 +1,9 @@ -# This is a FAKE secrets file for testing ContractGuard. -# None of these credentials are real. +# Intentionally unsafe sample values for ContractGuard demos. +# These are placeholders and should not match provider-issued token formats. -# AWS Credentials -AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE -AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY - -# GitHub Personal Access Token -GITHUB_TOKEN=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh - -# Stripe -STRIPE_SECRET_KEY=DEMO_FAKE_KEY_NOT_A_REAL_SECRET_0000000000000 - -# Slack Bot Token -SLACK_BOT_TOKEN=DEMO_FAKE_TOKEN_NOT_A_REAL_SECRET_0000000000000 - -# Database URL with embedded password -DATABASE_URL=postgresql://admin:SuperS3cret!@prod-db.example.com:5432/myapp - -# Google Cloud Service Account Key (partial) -GCP_API_KEY=AIzaSyA1234567890abcdefghijklmnopqrstuv - -# Private Key (truncated for demo) ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF0PbnGcY5unA1LBT0ePYlOG5TTML -ExampleKeyDataHereNotRealAtAll ------END RSA PRIVATE KEY----- - -# JWT Token (expired, fake) -AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ - -# npm token -//registry.npmjs.org/:_authToken=npm_ABCDEFghijklMNOPqrstuvwxyz123456 - -# Generic password in config +DATABASE_URL=postgresql://demo_user:demo-password@prod-db.example.com:5432/myapp DB_PASSWORD=admin123 REDIS_PASSWORD=r3d1s_p@ss! API_SECRET=my-super-secret-api-key-12345 +SERVICE_TOKEN=temporary-demo-token-value +JWT_SECRET=demo-signing-secret diff --git a/src/contractguard/__init__.py b/src/contractguard/__init__.py index 17b6c9f..33bcc5a 100644 --- a/src/contractguard/__init__.py +++ b/src/contractguard/__init__.py @@ -1,3 +1,3 @@ -"""ContractGuard — Stop bad inputs before they break your systems.""" +"""ContractGuard core package.""" -__version__ = "2.0.0" +__version__ = "3.0.0" diff --git a/src/contractguard/analyzers/csv_analyzer.py b/src/contractguard/analyzers/csv_analyzer.py deleted file mode 100644 index 3b9e01c..0000000 --- a/src/contractguard/analyzers/csv_analyzer.py +++ /dev/null @@ -1,154 +0,0 @@ -"""CSV Schema Analyzer. - -Detects type inconsistencies across rows, missing values, duplicate primary keys, -column count mismatches, and encoding issues in CSV data. -""" - -from __future__ import annotations - -import csv -import io -import re -from collections import Counter, defaultdict -from pathlib import Path -from typing import Any - -from contractguard.engine import Finding, load_rules_for_analyzer, run_rules - - -def _infer_type(value: str) -> str: - """Infer the type of a CSV cell value.""" - if value == "" or value.lower() in ("null", "none", "na", "n/a", "nan", ""): - return "null" - if value.lower() in ("true", "false", "yes", "no"): - return "boolean" - try: - int(value) - return "integer" - except ValueError: - pass - try: - float(value) - return "number" - except ValueError: - pass - if re.match(r"^\d{4}-\d{2}-\d{2}", value): - return "date" - if re.match(r"^\d{1,2}/\d{1,2}/\d{2,4}$", value): - return "date" - return "string" - - -def extract_facts(content: str, filename: str = "") -> dict[str, Any]: - """Analyze CSV content and extract facts.""" - facts: dict[str, Any] = { - "row_count": 0, - "column_count": 0, - "has_header": True, - "inconsistent_column_count": False, - "missing_value_count": 0, - "duplicate_rows": 0, - "mixed_type_columns": 0, - "null_heavy_columns": 0, - "column_type_map": {}, - "has_encoding_issues": False, - "empty_rows": 0, - "max_column_count_variance": 0, - } - - if "\ufffd" in content or "\x00" in content: - facts["has_encoding_issues"] = True - - try: - reader = csv.reader(io.StringIO(content)) - rows = list(reader) - except csv.Error: - facts["has_encoding_issues"] = True - return facts - - if not rows: - return facts - - header = rows[0] - data_rows = rows[1:] - facts["column_count"] = len(header) - facts["row_count"] = len(data_rows) - - column_counts = Counter(len(r) for r in data_rows) - if len(column_counts) > 1: - facts["inconsistent_column_count"] = True - counts = list(column_counts.keys()) - facts["max_column_count_variance"] = max(counts) - min(counts) - - col_types: dict[str, Counter] = defaultdict(Counter) - missing_per_col: dict[str, int] = defaultdict(int) - - for row in data_rows: - for i, cell in enumerate(row): - col_name = header[i] if i < len(header) else f"col_{i}" - cell_type = _infer_type(cell.strip()) - col_types[col_name][cell_type] += 1 - if cell_type == "null": - missing_per_col[col_name] += 1 - - # Count columns with mixed types (excluding null) - for col_name, type_counter in col_types.items(): - non_null_types = {t for t in type_counter if t != "null"} - if len(non_null_types) > 1: - facts["mixed_type_columns"] += 1 - - # Null-heavy columns (>50% null) - for col_name, null_count in missing_per_col.items(): - if facts["row_count"] > 0 and null_count / facts["row_count"] > 0.5: - facts["null_heavy_columns"] += 1 - - facts["missing_value_count"] = sum(missing_per_col.values()) - facts["column_type_map"] = {k: dict(v) for k, v in col_types.items()} - - row_tuples = [tuple(r) for r in data_rows] - facts["duplicate_rows"] = len(row_tuples) - len(set(row_tuples)) - - facts["empty_rows"] = sum(1 for r in data_rows if all(c.strip() == "" for c in r)) - - # Pre-compute for rule engine - for col_name in col_types: - non_null = {t for t in col_types[col_name] if t != "null"} - facts[f"field_types('{col_name}')"] = len(non_null) - - return facts - - -def load_csv_files(path: str | Path) -> list[tuple[str, str]]: - """Load CSV files from a file or directory.""" - path = Path(path) - files: list[tuple[str, str]] = [] - - if path.is_dir(): - for f in sorted(path.glob("*.csv")): - try: - files.append((str(f), f.read_text(encoding="utf-8", errors="replace"))) - except Exception: - continue - elif path.is_file(): - try: - files.append((str(path), path.read_text(encoding="utf-8", errors="replace"))) - except Exception: - pass - return files - - -def analyze(path: str | Path, rules_dir: str | Path) -> list[Finding]: - """Run CSV analysis on files at *path*.""" - files = load_csv_files(path) - rules = load_rules_for_analyzer(rules_dir, "csv") - all_findings: list[Finding] = [] - - for source, content in files: - facts = extract_facts(content, source) - findings = run_rules(facts, rules) - for f in findings: - f.location = source - f.context = f"{facts['row_count']} rows, {facts['column_count']} columns" - all_findings.extend(findings) - - return all_findings diff --git a/src/contractguard/bridge.py b/src/contractguard/bridge.py new file mode 100644 index 0000000..6b1d276 --- /dev/null +++ b/src/contractguard/bridge.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import typer + +from contractguard.scan import ScanTarget, findings_to_json, scan_target + +app = typer.Typer( + name="contractguard-bridge", + help="Machine-readable bridge used by the ContractGuard VS Code extension.", + add_completion=False, +) + + +@app.command("scan") +def scan( + path: Path = typer.Option(..., "--path", help="File or directory to scan."), + analyzer: str = typer.Option("all", "--analyzer", help="Analyzer id or 'all'."), + rules_dir: Path | None = typer.Option(None, "--rules-dir", help="Override rules directory."), + db_path: str | None = typer.Option(None, "--db", help="SQLite database used for SQL EXPLAIN mode."), + include_sarif: bool = typer.Option(False, "--include-sarif", help="Include SARIF payload in the response."), +) -> None: + result = scan_target( + ScanTarget(path=path, analyzer=analyzer, rules_dir=rules_dir, db_path=db_path), + include_sarif=include_sarif, + ) + typer.echo(json.dumps(result.to_dict(), indent=2)) + + +@app.command("findings") +def findings( + path: Path = typer.Option(..., "--path", help="File or directory to scan."), + analyzer: str = typer.Option("all", "--analyzer", help="Analyzer id or 'all'."), + rules_dir: Path | None = typer.Option(None, "--rules-dir", help="Override rules directory."), + db_path: str | None = typer.Option(None, "--db", help="SQLite database used for SQL EXPLAIN mode."), +) -> None: + result = scan_target(ScanTarget(path=path, analyzer=analyzer, rules_dir=rules_dir, db_path=db_path)) + typer.echo(findings_to_json(result.findings)) + + +if __name__ == "__main__": + app() diff --git a/src/contractguard/cli.py b/src/contractguard/cli.py index 9007789..efad9bc 100644 --- a/src/contractguard/cli.py +++ b/src/contractguard/cli.py @@ -1,20 +1,8 @@ -"""ContractGuard CLI — powered by Typer. - -Usage: - contractguard analyze --type json --path samples/json/ - contractguard analyze --type sql --path samples/sql/ --report report.html - contractguard analyze --type secrets --path . - contractguard analyze --type all --path . --report full-report.html - contractguard score --path . - contractguard history - contractguard watch --path . --type all - contractguard serve -""" +"""ContractGuard command-line interface.""" from __future__ import annotations import json -import sys import time from pathlib import Path from typing import Optional @@ -26,132 +14,68 @@ from contractguard import __version__ from contractguard.engine import Finding, Severity +from contractguard.scan import list_analyzers, resolve_rules_dir, run_scan, serialize_finding app = typer.Typer( name="contractguard", - help="🛡️ ContractGuard — Stop bad inputs before they break your systems.", + help="ContractGuard security analysis for code, config, and build assets.", add_completion=False, ) console = Console() -_RULES_DIR_DEFAULT = Path(__file__).resolve().parent.parent.parent / "rules" - -_ANALYZER_TYPES = [ - "json", "sql", "regex", "secrets", "pii", "csv", "config", "dockerfile", "deps", "all", -] - - -def _resolve_rules_dir(rules_dir: Path | None) -> Path: - """Find the rules directory — check common locations.""" - if rules_dir and rules_dir.exists(): - return rules_dir - cwd_rules = Path.cwd() / "rules" - if cwd_rules.exists(): - return cwd_rules - if _RULES_DIR_DEFAULT.exists(): - return _RULES_DIR_DEFAULT - console.print("[red]Error:[/red] Cannot find rules/ directory. Use --rules-dir.") - raise typer.Exit(1) +_ANALYZER_TYPES = [*list_analyzers(), "all"] def _severity_color(sev: Severity) -> str: return { - "info": "blue", "warning": "yellow", "critical": "red", "block": "bright_red", + "info": "blue", + "warning": "yellow", + "critical": "red", + "block": "bright_red", }.get(sev.value, "white") -def _run_analyzer(analyzer_type: str, path: Path, rules_path: Path, db: str | None = None) -> list[Finding]: - """Run a single analyzer type and return findings.""" - if analyzer_type == "json": - from contractguard.analyzers.json_analyzer import analyze as fn - return fn(path, rules_path) - elif analyzer_type == "sql": - from contractguard.analyzers.sql_analyzer import analyze as fn - return fn(path, rules_path, db_path=db) - elif analyzer_type == "regex": - from contractguard.analyzers.regex_analyzer import analyze as fn - return fn(path, rules_path) - elif analyzer_type == "secrets": - from contractguard.analyzers.secrets_analyzer import analyze as fn - return fn(path, rules_path) - elif analyzer_type == "pii": - from contractguard.analyzers.pii_analyzer import analyze as fn - return fn(path, rules_path) - elif analyzer_type == "csv": - from contractguard.analyzers.csv_analyzer import analyze as fn - return fn(path, rules_path) - elif analyzer_type == "config": - from contractguard.analyzers.config_analyzer import analyze as fn - return fn(path, rules_path) - elif analyzer_type == "dockerfile": - from contractguard.analyzers.dockerfile_analyzer import analyze as fn - return fn(path, rules_path) - elif analyzer_type == "deps": - from contractguard.analyzers.dependency_analyzer import analyze as fn - return fn(path, rules_path) - return [] - - -def _run_all_analyzers(path: Path, rules_path: Path) -> list[Finding]: - """Run ALL analyzers and aggregate findings.""" - all_findings: list[Finding] = [] - for atype in _ANALYZER_TYPES: - if atype == "all": - continue - try: - findings = _run_analyzer(atype, path, rules_path) - all_findings.extend(findings) - except Exception as e: - console.print(f"[dim]Analyzer '{atype}' skipped: {e}[/dim]") - return all_findings - - def _print_findings(findings: list[Finding], ci_mode: bool = False) -> bool: - """Print findings to the console using rich tables. Returns True if CI should fail.""" if not findings: - console.print("[green]✓ No issues found.[/green]") + console.print("[green]No issues found.[/green]") return False - table = Table(title="🛡️ ContractGuard Findings", show_lines=True) + table = Table(title="ContractGuard Findings", show_lines=True) table.add_column("ID", style="bold") table.add_column("Severity") table.add_column("Description", max_width=50) table.add_column("Location", max_width=40) table.add_column("Suggestion", max_width=50) - for f in findings: - color = _severity_color(f.severity) - sev_label = f.severity.value.upper() - if f.severity == Severity.BLOCK: - sev_label = "🚫 BLOCK" + for finding in findings: + color = _severity_color(finding.severity) table.add_row( - f.rule_id, - f"[{color}]{sev_label}[/{color}]", - f.description, - f.location, - f.suggestion, + finding.rule_id, + f"[{color}]{finding.severity.value.upper()}[/{color}]", + finding.description, + finding.location, + finding.suggestion, ) console.print(table) - blocks = sum(1 for f in findings if f.severity == Severity.BLOCK) - crits = sum(1 for f in findings if f.severity == Severity.CRITICAL) - warns = sum(1 for f in findings if f.severity == Severity.WARNING) - infos = sum(1 for f in findings if f.severity == Severity.INFO) + blocks = sum(1 for item in findings if item.severity == Severity.BLOCK) + critical = sum(1 for item in findings if item.severity == Severity.CRITICAL) + warning = sum(1 for item in findings if item.severity == Severity.WARNING) + info = sum(1 for item in findings if item.severity == Severity.INFO) console.print( - f"\n[bold]Summary:[/bold] {len(findings)} finding(s) — " + f"\n[bold]Summary:[/bold] {len(findings)} finding(s) - " f"[bright_red]{blocks} block[/bright_red], " - f"[red]{crits} critical[/red], [yellow]{warns} warning[/yellow], [blue]{infos} info[/blue]" + f"[red]{critical} critical[/red], [yellow]{warning} warning[/yellow], [blue]{info} info[/blue]" ) - if ci_mode and (blocks > 0 or crits > 0): - console.print("[red bold]CI mode: failing due to critical/block findings.[/red bold]") + if ci_mode and (blocks > 0 or critical > 0): + console.print("[red bold]CI mode: failing due to critical or block findings.[/red bold]") return True return False def _print_score(findings: list[Finding]) -> None: - """Print the security score summary.""" from contractguard.scorer import compute_score score = compute_score(findings) @@ -159,19 +83,29 @@ def _print_score(findings: list[Finding]) -> None: color = grade_colors.get(score.grade, "white") console.print() - console.print(Panel( - f"[{color} bold] Grade: {score.grade} | Score: {score.score}/100 [/{color} bold]\n\n" - f" {score.risk_summary}\n\n" - f" Findings: {score.total_findings} total — " - f"[bright_red]{score.block_count} BLOCK[/bright_red], " - f"[red]{score.critical_count} CRITICAL[/red], " - f"[yellow]{score.warning_count} WARNING[/yellow], " - f"[blue]{score.info_count} INFO[/blue]" - + (f"\n\n [bold]Attack Surface:[/bold] {', '.join(score.attack_surface[:5])}" if score.attack_surface else "") - + (f"\n\n [bold]Top Risks:[/bold]\n" + "\n".join(f" • {r}" for r in score.top_risks) if score.top_risks else ""), - title="🛡️ ContractGuard Security Score", - border_style=color, - )) + console.print( + Panel( + f"[{color} bold] Grade: {score.grade} | Score: {score.score}/100 [/{color} bold]\n\n" + f" {score.risk_summary}\n\n" + f" Findings: {score.total_findings} total - " + f"[bright_red]{score.block_count} BLOCK[/bright_red], " + f"[red]{score.critical_count} CRITICAL[/red], " + f"[yellow]{score.warning_count} WARNING[/yellow], " + f"[blue]{score.info_count} INFO[/blue]" + + ( + f"\n\n [bold]Attack Surface:[/bold] {', '.join(score.attack_surface[:5])}" + if score.attack_surface + else "" + ) + + ( + f"\n\n [bold]Top Risks:[/bold]\n" + "\n".join(f" - {risk}" for risk in score.top_risks) + if score.top_risks + else "" + ), + title="ContractGuard Security Score", + border_style=color, + ) + ) @app.command() @@ -181,14 +115,17 @@ def analyze( rules_dir: Optional[Path] = typer.Option(None, "--rules-dir", "-r", help="Path to rules/ directory"), report: Optional[Path] = typer.Option(None, "--report", help="Write HTML report to this path"), report_json: Optional[Path] = typer.Option(None, "--report-json", help="Write JSON report"), - report_sarif: Optional[Path] = typer.Option(None, "--report-sarif", help="Write SARIF report (GitHub Code Scanning)"), + report_sarif: Optional[Path] = typer.Option(None, "--report-sarif", help="Write SARIF report"), db: Optional[str] = typer.Option(None, "--db", help="SQLite DB path for EXPLAIN mode (sql only)"), - ci: bool = typer.Option(False, "--ci", help="CI mode: exit code 2 on critical/block findings"), + ci: bool = typer.Option(False, "--ci", help="CI mode: exit code 2 on critical or block findings"), show_score: bool = typer.Option(False, "--score", help="Show security grade after analysis"), record: bool = typer.Option(False, "--record", help="Record scan to history database"), ) -> None: - """Analyze inputs and flag reliability/safety issues.""" - rules_path = _resolve_rules_dir(rules_dir) + try: + rules_path = resolve_rules_dir(rules_dir) + except FileNotFoundError as exc: + console.print(f"[red]Error:[/red] {exc}") + raise typer.Exit(1) if not path.exists(): console.print(f"[red]Error:[/red] path does not exist: {path}") @@ -198,11 +135,7 @@ def analyze( console.print(f"[red]Error:[/red] Unknown type '{type}'. Use: {', '.join(_ANALYZER_TYPES)}") raise typer.Exit(1) - if type == "all": - findings = _run_all_analyzers(path, rules_path) - else: - findings = _run_analyzer(type, path, rules_path, db=db) - + findings = run_scan(path=path, analyzer=type, rules_dir=rules_path, db_path=db) ci_fail = _print_findings(findings, ci_mode=ci) if show_score or type == "all": @@ -210,36 +143,24 @@ def analyze( if record: from contractguard.history import record_scan + score = record_scan(findings, analyzer=type, source_path=str(path)) console.print(f"[dim]Scan recorded. Grade: {score.grade} ({score.score}/100)[/dim]") if report: from contractguard.reporter import render_html_report + html = render_html_report(findings, analyzer_type=type, source_path=str(path)) report.write_text(html, encoding="utf-8") console.print(f"[green]HTML report written to {report}[/green]") if report_json: - data = [ - { - "rule_id": f.rule_id, - "rule_name": f.rule_name, - "severity": f.severity.value, - "description": f.description, - "explanation": f.explanation, - "suggestion": f.suggestion, - "location": f.location, - "context": f.context, - "attack_vector": f.attack_vector, - "cwe": f.cwe, - } - for f in findings - ] - report_json.write_text(json.dumps(data, indent=2), encoding="utf-8") + report_json.write_text(json.dumps([serialize_finding(item) for item in findings], indent=2), encoding="utf-8") console.print(f"[green]JSON report written to {report_json}[/green]") if report_sarif: from contractguard.reporter import render_sarif_report + sarif = render_sarif_report(findings, analyzer_type=type) report_sarif.write_text(json.dumps(sarif, indent=2), encoding="utf-8") console.print(f"[green]SARIF report written to {report_sarif}[/green]") @@ -253,14 +174,18 @@ def score( path: Path = typer.Option(".", "--path", "-p", help="Project root to scan"), rules_dir: Optional[Path] = typer.Option(None, "--rules-dir", "-r"), ) -> None: - """Run all analyzers and display overall security grade.""" - rules_path = _resolve_rules_dir(rules_dir) + try: + rules_path = resolve_rules_dir(rules_dir) + except FileNotFoundError as exc: + console.print(f"[red]Error:[/red] {exc}") + raise typer.Exit(1) + if not path.exists(): console.print(f"[red]Error:[/red] path does not exist: {path}") raise typer.Exit(1) console.print("[bold]Running full security scan...[/bold]") - findings = _run_all_analyzers(path, rules_path) + findings = run_scan(path=path, analyzer="all", rules_dir=rules_path) _print_score(findings) @@ -269,7 +194,6 @@ def history( limit: int = typer.Option(20, "--limit", "-n", help="Number of scans to show"), db_path: Optional[Path] = typer.Option(None, "--db", help="History database path"), ) -> None: - """Show scan history and trend analysis.""" from contractguard.history import get_history, get_trend records = get_history(limit=limit, db_path=db_path) @@ -285,24 +209,26 @@ def history( table.add_column("Findings") table.add_column("Source", max_width=40) - for r in records: + for record in records: grade_colors = {"A": "green", "B": "green", "C": "yellow", "D": "red", "F": "bright_red"} - color = grade_colors.get(r["grade"], "white") + color = grade_colors.get(record["grade"], "white") table.add_row( - r["timestamp"][:19], - r["analyzer"], - f"[{color}]{r['grade']}[/{color}]", - str(r["score"]), - str(r["total_findings"]), - r["source_path"], + record["timestamp"][:19], + record["analyzer"], + f"[{color}]{record['grade']}[/{color}]", + str(record["score"]), + str(record["total_findings"]), + record["source_path"], ) console.print(table) trend = get_trend(db_path=db_path) if trend["trend"] != "no_data": - trend_icons = {"improving": "📈", "degrading": "📉", "stable": "➡️"} + trend_icons = {"improving": "+", "degrading": "-", "stable": "="} icon = trend_icons.get(trend["trend"], "") - console.print(f"\n[bold]Trend:[/bold] {icon} {trend['trend'].upper()} — Latest: {trend['latest_grade']} ({trend['latest_score']}/100)") + console.print( + f"\n[bold]Trend:[/bold] {icon} {trend['trend'].upper()} - Latest: {trend['latest_grade']} ({trend['latest_score']}/100)" + ) @app.command() @@ -312,42 +238,41 @@ def watch( rules_dir: Optional[Path] = typer.Option(None, "--rules-dir", "-r"), interval: int = typer.Option(3, "--interval", help="Seconds between scans"), ) -> None: - """Watch files and re-run analysis on changes.""" - rules_path = _resolve_rules_dir(rules_dir) + try: + rules_path = resolve_rules_dir(rules_dir) + except FileNotFoundError as exc: + console.print(f"[red]Error:[/red] {exc}") + raise typer.Exit(1) + if not path.exists(): console.print(f"[red]Error:[/red] path does not exist: {path}") raise typer.Exit(1) console.print(f"[bold]Watching {path} (every {interval}s). Press Ctrl+C to stop.[/bold]") - last_mtimes: dict[str, float] = {} - - def _get_mtimes() -> dict[str, float]: + def get_mtimes() -> dict[str, float]: mtimes: dict[str, float] = {} target = path if path.is_dir() else path.parent - for f in target.rglob("*"): - if f.is_file() and not any(p.startswith(".") for p in f.parts): + for file_path in target.rglob("*"): + if file_path.is_file() and not any(part.startswith(".") for part in file_path.parts): try: - mtimes[str(f)] = f.stat().st_mtime + mtimes[str(file_path)] = file_path.stat().st_mtime except OSError: - pass + continue return mtimes - last_mtimes = _get_mtimes() + last_mtimes = get_mtimes() try: while True: time.sleep(interval) - current = _get_mtimes() - changed = {k for k in current if current.get(k) != last_mtimes.get(k)} + current = get_mtimes() + changed = {key for key in current if current.get(key) != last_mtimes.get(key)} new_files = set(current) - set(last_mtimes) if changed or new_files: console.print(f"\n[yellow]Change detected ({len(changed | new_files)} file(s)). Re-scanning...[/yellow]") - if type == "all": - findings = _run_all_analyzers(path, rules_path) - else: - findings = _run_analyzer(type, path, rules_path) + findings = run_scan(path=path, analyzer=type, rules_dir=rules_path) _print_findings(findings) _print_score(findings) last_mtimes = current @@ -357,21 +282,8 @@ def _get_mtimes() -> dict[str, float]: @app.command() def version() -> None: - """Print the version number.""" console.print(f"ContractGuard v{__version__}") -@app.command() -def serve( - host: str = typer.Option("127.0.0.1", help="Host to bind to"), - port: int = typer.Option(8000, help="Port to bind to"), -) -> None: - """Launch the ContractGuard web UI.""" - console.print(f"[bold]Starting ContractGuard web UI on http://{host}:{port}[/bold]") - import uvicorn - from contractguard.web import create_app - uvicorn.run(create_app(), host=host, port=port) - - if __name__ == "__main__": app() diff --git a/src/contractguard/reporter.py b/src/contractguard/reporter.py index 49cecc3..0b9d31b 100644 --- a/src/contractguard/reporter.py +++ b/src/contractguard/reporter.py @@ -1,19 +1,14 @@ -"""HTML, JSON, & SARIF report generation. - -Produces: -- Self-contained HTML report with security grade, attack vectors, dark aggressive theme -- SARIF 2.1.0 output for GitHub Code Scanning integration -""" +"""Report generation for HTML and SARIF outputs.""" from __future__ import annotations import datetime from typing import Any -from jinja2 import Environment, BaseLoader +from jinja2 import BaseLoader, Environment from contractguard.engine import Finding, Severity -from contractguard.scorer import SecurityScore, compute_score +from contractguard.scorer import compute_score _HTML_TEMPLATE = r""" @@ -23,110 +18,72 @@ ContractGuard Security Report
-

🛡 ContractGuard Security Report

-

Stop bad inputs before they break your systems.

-

Analyzer: {{ analyzer_type }}  |  - Source: {{ source_path }}  |  - Generated: {{ timestamp }}

- - -
-
{{ grade }}
-
-
Security Score: {{ score_value }}/100
-
{{ risk_summary }}
+

ContractGuard Security Report

+

Security analysis for source code, configs, queries, and build assets.

+

Analyzer: {{ analyzer_type }} | Source: {{ source_path }} | Generated: {{ timestamp }}

+ +
+
{{ grade }}
+
+

Security Score: {{ score_value }}/100

+

{{ risk_summary }}

-
-
{{ total }}
Total
-
{{ block }}
🚫 Block
-
{{ critical }}
Critical
-
{{ warning }}
Warning
-
{{ info }}
Info
+
+
{{ total }}
Total
+
{{ block }}
Block
+
{{ critical }}
Critical
+
{{ warning }}
Warning
+
{{ info }}
Info
{% if attack_surface %} -
-

⚠️ Attack Surface Identified

- {% for a in attack_surface %}{{ a }}{% endfor %} +
+

Attack Surface

+ {% for entry in attack_surface %}{{ entry }}{% endfor %} {% if top_risks %} -
    - {% for r in top_risks %}
  • {{ r }}
  • {% endfor %} +
      + {% for risk in top_risks %}
    • {{ risk }}
    • {% endfor %}
    {% endif %}
@@ -135,27 +92,27 @@ {% if findings %} - + - {% for f in findings %} + {% for finding in findings %} - - - - - - - + + + + + + + {% endfor %}
IDSeverityCWEDescriptionLocationAttack VectorSuggestion
IDSeverityCWEDescriptionLocationContextSuggestion
{{ f.rule_id }}{{ f.severity.value }}{{ f.cwe }}{{ f.description }}{{ f.location }}
{{ f.context | truncate(60) }}
{{ f.attack_vector | truncate(80) }}{{ f.suggestion }}{{ finding.rule_id }}{{ finding.severity.value }}{{ finding.cwe }}{{ finding.description }}{{ finding.location }}{{ finding.context | truncate(60) }}{{ finding.suggestion }}
{% else %} -
✅ All clear — no issues found.
+
All clear: no issues found.
{% endif %} -
ContractGuard v1.0.0 — Built for DevPost Season of Code
+
ContractGuard report
""" @@ -166,27 +123,20 @@ def render_html_report( analyzer_type: str = "", source_path: str = "", ) -> str: - """Render findings to a self-contained HTML report with security grade.""" env = Environment(loader=BaseLoader(), autoescape=True) template = env.from_string(_HTML_TEMPLATE) score_obj = compute_score(findings) - - block = sum(1 for f in findings if f.severity == Severity.BLOCK) - critical = sum(1 for f in findings if f.severity == Severity.CRITICAL) - warning = sum(1 for f in findings if f.severity == Severity.WARNING) - info = sum(1 for f in findings if f.severity == Severity.INFO) - return template.render( findings=findings, analyzer_type=analyzer_type, source_path=source_path, timestamp=datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC"), total=len(findings), - block=block, - critical=critical, - warning=warning, - info=info, + block=sum(1 for item in findings if item.severity == Severity.BLOCK), + critical=sum(1 for item in findings if item.severity == Severity.CRITICAL), + warning=sum(1 for item in findings if item.severity == Severity.WARNING), + info=sum(1 for item in findings if item.severity == Severity.INFO), grade=score_obj.grade, score_value=score_obj.score, risk_summary=score_obj.risk_summary, @@ -199,7 +149,6 @@ def render_sarif_report( findings: list[Finding], analyzer_type: str = "", ) -> dict[str, Any]: - """Render findings as SARIF 2.1.0 for GitHub Code Scanning integration.""" severity_map = { Severity.INFO: "note", Severity.WARNING: "warning", @@ -207,62 +156,66 @@ def render_sarif_report( Severity.BLOCK: "error", } - rules: list[dict] = [] - results: list[dict] = [] + rules: list[dict[str, Any]] = [] + results: list[dict[str, Any]] = [] seen_rule_ids: set[str] = set() - for f in findings: - if f.rule_id not in seen_rule_ids: - seen_rule_ids.add(f.rule_id) + for finding in findings: + if finding.rule_id not in seen_rule_ids: + seen_rule_ids.add(finding.rule_id) rule_def: dict[str, Any] = { - "id": f.rule_id, - "name": f.rule_name, - "shortDescription": {"text": f.description}, - "defaultConfiguration": { - "level": severity_map.get(f.severity, "warning"), - }, - "helpUri": f"https://cwe.mitre.org/data/definitions/{f.cwe.replace('CWE-', '')}.html" if f.cwe else "", + "id": finding.rule_id, + "name": finding.rule_name, + "shortDescription": {"text": finding.description}, + "defaultConfiguration": {"level": severity_map.get(finding.severity, "warning")}, } - if f.attack_vector: - rule_def["fullDescription"] = {"text": f"Attack vector: {f.attack_vector}"} + if finding.cwe: + rule_def["helpUri"] = f"https://cwe.mitre.org/data/definitions/{finding.cwe.replace('CWE-', '')}.html" + if finding.attack_vector: + rule_def["fullDescription"] = {"text": f"Attack vector: {finding.attack_vector}"} rules.append(rule_def) - file_path = f.location.split(":")[0] if f.location else "" + file_path = finding.location.split(":")[0] if finding.location else "" line = 1 - if ":" in f.location: - parts = f.location.rsplit(":", 1) + if ":" in finding.location: + parts = finding.location.rsplit(":", 1) try: line = int(parts[1]) except ValueError: - pass + line = 1 result: dict[str, Any] = { - "ruleId": f.rule_id, - "level": severity_map.get(f.severity, "warning"), - "message": {"text": f"{f.description} — {f.suggestion}"}, - "locations": [{ - "physicalLocation": { - "artifactLocation": {"uri": file_path.replace("\\", "/")}, - "region": {"startLine": line}, + "ruleId": finding.rule_id, + "level": severity_map.get(finding.severity, "warning"), + "message": {"text": f"{finding.description} - {finding.suggestion}"}, + "locations": [ + { + "physicalLocation": { + "artifactLocation": {"uri": file_path.replace("\\", "/")}, + "region": {"startLine": line}, + } } - }], + ], } - if f.cwe: - result["taxa"] = [{"id": f.cwe, "toolComponent": {"name": "CWE"}}] + if finding.cwe: + result["taxa"] = [{"id": finding.cwe, "toolComponent": {"name": "CWE"}}] results.append(result) return { "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", "version": "2.1.0", - "runs": [{ - "tool": { - "driver": { - "name": "ContractGuard", - "version": "1.0.0", - "informationUri": "https://github.com/contractguard", - "rules": rules, - } - }, - "results": results, - }], + "runs": [ + { + "tool": { + "driver": { + "name": "ContractGuard", + "version": "3.0.0", + "informationUri": "https://github.com/contractguard/contractguard", + "rules": rules, + } + }, + "invocations": [{"commandLine": analyzer_type or "all"}], + "results": results, + } + ], } diff --git a/src/contractguard/scan.py b/src/contractguard/scan.py new file mode 100644 index 0000000..b2c5250 --- /dev/null +++ b/src/contractguard/scan.py @@ -0,0 +1,187 @@ +from __future__ import annotations + +import importlib +import json +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any, Callable + +from contractguard import __version__ +from contractguard.engine import Finding, Severity +from contractguard.reporter import render_sarif_report +from contractguard.scorer import SecurityScore, compute_score + +AnalyzerFn = Callable[..., list[Finding]] + +ANALYZER_IDS = ( + "json", + "sql", + "regex", + "secrets", + "pii", + "config", + "dockerfile", + "deps", +) + +DEFAULT_RULES_DIR = Path(__file__).resolve().parent.parent.parent / "rules" + + +@dataclass(frozen=True) +class ScanTarget: + path: Path + analyzer: str = "all" + rules_dir: Path | None = None + db_path: str | None = None + + +@dataclass +class ScanResult: + target: str + analyzer: str + findings: list[Finding] + score: SecurityScore + sarif: dict[str, Any] | None = None + generated_at: str | None = None + + def to_dict(self) -> dict[str, Any]: + return { + "target": self.target, + "analyzer": self.analyzer, + "generated_at": self.generated_at, + "engine_version": __version__, + "score": asdict(self.score), + "findings": [serialize_finding(item) for item in self.findings], + "sarif": self.sarif, + } + + +def resolve_rules_dir(rules_dir: Path | None = None) -> Path: + if rules_dir and rules_dir.exists(): + return rules_dir + cwd_rules = Path.cwd() / "rules" + if cwd_rules.exists(): + return cwd_rules + if DEFAULT_RULES_DIR.exists(): + return DEFAULT_RULES_DIR + raise FileNotFoundError("Could not locate the ContractGuard rules directory.") + + +def _get_analyzer_registry() -> dict[str, str]: + return { + "json": "contractguard.analyzers.json_analyzer", + "sql": "contractguard.analyzers.sql_analyzer", + "regex": "contractguard.analyzers.regex_analyzer", + "secrets": "contractguard.analyzers.secrets_analyzer", + "pii": "contractguard.analyzers.pii_analyzer", + "config": "contractguard.analyzers.config_analyzer", + "dockerfile": "contractguard.analyzers.dockerfile_analyzer", + "deps": "contractguard.analyzers.dependency_analyzer", + } + + +def list_analyzers() -> tuple[str, ...]: + return ANALYZER_IDS + + +def serialize_finding(finding: Finding) -> dict[str, Any]: + return { + "rule_id": finding.rule_id, + "rule_name": finding.rule_name, + "severity": finding.severity.value, + "description": finding.description, + "explanation": finding.explanation, + "suggestion": finding.suggestion, + "location": finding.location, + "context": finding.context, + "attack_vector": finding.attack_vector, + "cwe": finding.cwe, + "confidence": finding.confidence, + } + + +def findings_to_json(findings: list[Finding]) -> str: + return json.dumps([serialize_finding(item) for item in findings], indent=2) + + +def scan_target(target: ScanTarget, include_sarif: bool = False) -> ScanResult: + path = target.path + if not path.exists(): + raise FileNotFoundError(f"Scan target does not exist: {path}") + + analyzer = target.analyzer + if analyzer != "all" and analyzer not in ANALYZER_IDS: + supported = ", ".join((*ANALYZER_IDS, "all")) + raise ValueError(f"Unsupported analyzer '{analyzer}'. Supported values: {supported}") + + rules_dir = resolve_rules_dir(target.rules_dir) + findings = run_scan(path=path, analyzer=analyzer, rules_dir=rules_dir, db_path=target.db_path) + score = compute_score(findings) + sarif = render_sarif_report(findings, analyzer_type=analyzer) if include_sarif else None + return ScanResult( + target=str(path), + analyzer=analyzer, + findings=findings, + score=score, + sarif=sarif, + ) + + +def run_scan( + path: str | Path, + analyzer: str = "all", + rules_dir: str | Path | None = None, + db_path: str | None = None, +) -> list[Finding]: + registry = _get_analyzer_registry() + rules_path = resolve_rules_dir(Path(rules_dir) if rules_dir else None) + target_path = Path(path) + + if analyzer == "all": + findings: list[Finding] = [] + for analyzer_id, module_path in registry.items(): + findings.extend( + _invoke_analyzer( + analyzer_id=analyzer_id, + analyzer_fn=_load_analyzer(module_path), + path=target_path, + rules_path=rules_path, + db_path=db_path, + ) + ) + return findings + + return _invoke_analyzer( + analyzer_id=analyzer, + analyzer_fn=_load_analyzer(registry[analyzer]), + path=target_path, + rules_path=rules_path, + db_path=db_path, + ) + + +def _load_analyzer(module_path: str) -> AnalyzerFn: + module = importlib.import_module(module_path) + return getattr(module, "analyze") + + +def _invoke_analyzer( + analyzer_id: str, + analyzer_fn: AnalyzerFn, + path: Path, + rules_path: Path, + db_path: str | None, +) -> list[Finding]: + if analyzer_id == "sql": + return analyzer_fn(path, rules_path, db_path=db_path) + return analyzer_fn(path, rules_path) + + +def summarize_findings(findings: list[Finding]) -> dict[str, int]: + return { + "total": len(findings), + "block": sum(1 for item in findings if item.severity == Severity.BLOCK), + "critical": sum(1 for item in findings if item.severity == Severity.CRITICAL), + "warning": sum(1 for item in findings if item.severity == Severity.WARNING), + "info": sum(1 for item in findings if item.severity == Severity.INFO), + } diff --git a/src/contractguard/web.py b/src/contractguard/web.py deleted file mode 100644 index d8da521..0000000 --- a/src/contractguard/web.py +++ /dev/null @@ -1,164 +0,0 @@ -"""ContractGuard Web UI — FastAPI single-page app for uploading and analyzing inputs.""" - -from __future__ import annotations - -import tempfile -from pathlib import Path - -from fastapi import FastAPI, File, Form, UploadFile -from fastapi.responses import HTMLResponse - -from contractguard.engine import Finding -from contractguard.reporter import render_html_report - -_RULES_DIR = Path(__file__).resolve().parent.parent.parent / "rules" - -_UPLOAD_PAGE = """ - - - - -ContractGuard - - - -
-

🛡 ContractGuard

-

Stop bad inputs before they break your systems.

-
- - - - -
— or paste content directly —
- - - -
-
- -""" - - -def _resolve_rules_dir() -> Path: - """Find rules directory (relative to package or CWD).""" - if _RULES_DIR.exists(): - return _RULES_DIR - cwd_rules = Path.cwd() / "rules" - if cwd_rules.exists(): - return cwd_rules - return _RULES_DIR - - -def _run_analyzer(analyzer_type: str, path: Path, rules_dir: Path) -> list[Finding]: - """Run a single analyzer or all analyzers.""" - if analyzer_type == "all": - from contractguard.cli import _run_all_analyzers - return _run_all_analyzers(path, rules_dir) - if analyzer_type == "json": - from contractguard.analyzers.json_analyzer import analyze as fn - return fn(path, rules_dir) - if analyzer_type == "sql": - from contractguard.analyzers.sql_analyzer import analyze as fn - return fn(path, rules_dir) - if analyzer_type == "regex": - from contractguard.analyzers.regex_analyzer import analyze as fn - return fn(path, rules_dir) - if analyzer_type == "secrets": - from contractguard.analyzers.secrets_analyzer import analyze as fn - return fn(path, rules_dir) - if analyzer_type == "pii": - from contractguard.analyzers.pii_analyzer import analyze as fn - return fn(path, rules_dir) - if analyzer_type == "csv": - from contractguard.analyzers.csv_analyzer import analyze as fn - return fn(path, rules_dir) - if analyzer_type == "config": - from contractguard.analyzers.config_analyzer import analyze as fn - return fn(path, rules_dir) - if analyzer_type == "dockerfile": - from contractguard.analyzers.dockerfile_analyzer import analyze as fn - return fn(path, rules_dir) - if analyzer_type == "deps": - from contractguard.analyzers.dependency_analyzer import analyze as fn - return fn(path, rules_dir) - return [] - - -def create_app() -> FastAPI: - """Factory function for the FastAPI application.""" - app = FastAPI(title="ContractGuard", version="1.0.0") - - @app.get("/", response_class=HTMLResponse) - async def index(): - return _UPLOAD_PAGE - - @app.post("/analyze", response_class=HTMLResponse) - async def analyze_endpoint( - type: str = Form(...), - file: UploadFile | None = File(None), - content: str = Form(""), - ): - rules_dir = _resolve_rules_dir() - - raw = "" - if file and file.filename: - raw = (await file.read()).decode("utf-8", errors="replace") - elif content.strip(): - raw = content.strip() - else: - return HTMLResponse("

No input provided.

Go back", status_code=400) - - suffix_map = { - "json": ".json", "sql": ".sql", "regex": ".txt", "secrets": ".env", - "pii": ".json", "csv": ".csv", "config": ".yaml", "dockerfile": "", - "deps": ".txt", "all": ".txt", - } - suffix = suffix_map.get(type, ".txt") - with tempfile.NamedTemporaryFile(mode="w", suffix=suffix, delete=False, encoding="utf-8") as tmp: - tmp.write(raw) - tmp_path = Path(tmp.name) - - findings: list[Finding] = [] - try: - findings = _run_analyzer(type, tmp_path, rules_dir) - finally: - tmp_path.unlink(missing_ok=True) - - source_label = file.filename if file and file.filename else "pasted input" - html = render_html_report(findings, analyzer_type=type, source_path=source_label) - return HTMLResponse(html) - - @app.get("/health") - async def health(): - return {"status": "ok", "version": "1.0.0"} - - return app diff --git a/tests/test_bridge.py b/tests/test_bridge.py new file mode 100644 index 0000000..8f06411 --- /dev/null +++ b/tests/test_bridge.py @@ -0,0 +1,34 @@ +import json +import os +import subprocess +import sys +from pathlib import Path + + +def test_bridge_scan_outputs_json(): + repo_root = Path(__file__).resolve().parent.parent + target = repo_root / "samples" / "regex" + + result = subprocess.run( + [ + sys.executable, + "-m", + "contractguard.bridge", + "scan", + "--path", + str(target), + "--analyzer", + "regex", + "--rules-dir", + str(repo_root / "rules"), + ], + cwd=repo_root, + env={**os.environ, "PYTHONPATH": str(repo_root / "src")}, + capture_output=True, + text=True, + check=True, + ) + + payload = json.loads(result.stdout) + assert payload["analyzer"] == "regex" + assert "findings" in payload diff --git a/tests/test_csv_analyzer.py b/tests/test_csv_analyzer.py deleted file mode 100644 index deb478f..0000000 --- a/tests/test_csv_analyzer.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Tests for the CSV analyzer.""" - -from pathlib import Path -import tempfile - -import pytest - -from contractguard.analyzers.csv_analyzer import analyze, extract_facts - -RULES_DIR = Path(__file__).resolve().parent.parent / "rules" - - -class TestExtractFacts: - def test_basic_csv(self): - content = "name,age,score\nAlice,30,95\nBob,25,88\n" - facts = extract_facts(content) - assert facts["row_count"] == 2 - assert facts["column_count"] == 3 - assert facts["duplicate_rows"] == 0 - - def test_mixed_types(self): - content = "id,value\n1,hello\n2,42\n3,world\n" - facts = extract_facts(content) - # 'value' column has string and integer — mixed - assert facts["mixed_type_columns"] >= 1 - - def test_null_values(self): - content = "name,email\nAlice,a@b.com\nBob,\nCarol,\nDave,\n" - facts = extract_facts(content) - assert facts["missing_value_count"] >= 2 - - def test_duplicate_rows(self): - content = "a,b\n1,2\n1,2\n3,4\n" - facts = extract_facts(content) - assert facts["duplicate_rows"] == 1 - - def test_inconsistent_columns(self): - content = "a,b,c\n1,2,3\n4,5\n6,7,8,9\n" - facts = extract_facts(content) - assert facts["inconsistent_column_count"] is True - - def test_encoding_issues(self): - content = "name\nHello\x00World\n" - facts = extract_facts(content) - assert facts["has_encoding_issues"] is True - - def test_empty_csv(self): - content = "" - facts = extract_facts(content) - assert facts["row_count"] == 0 - - -class TestAnalyze: - def test_analyze_csv_file(self): - with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f: - f.write("id,name,value\n1,Alice,hello\n2,Bob,42\n1,Alice,hello\n") - path = Path(f.name) - try: - findings = analyze(path, RULES_DIR) - # Should find mixed types + duplicate rows - assert len(findings) >= 1 - finally: - path.unlink(missing_ok=True) - - def test_analyze_clean_csv(self): - with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f: - f.write("name,age\nAlice,30\nBob,25\nCarol,28\n") - path = Path(f.name) - try: - findings = analyze(path, RULES_DIR) - # Clean CSV with consistent types, no nulls, no dupes - assert len(findings) == 0 - finally: - path.unlink(missing_ok=True) - - def test_analyze_directory(self, tmp_path): - (tmp_path / "data.csv").write_text("a,b\n1,2\n1,2\n") - findings = analyze(tmp_path, RULES_DIR) - assert len(findings) >= 1 # duplicate rows diff --git a/tests/test_scan.py b/tests/test_scan.py new file mode 100644 index 0000000..72c9287 --- /dev/null +++ b/tests/test_scan.py @@ -0,0 +1,45 @@ +from pathlib import Path + +from contractguard.scan import ScanTarget, list_analyzers, scan_target, serialize_finding + + +def test_list_analyzers_excludes_csv(): + analyzers = list_analyzers() + assert "csv" not in analyzers + assert "json" in analyzers + + +def test_scan_target_returns_score_and_findings(tmp_path): + data_file = tmp_path / "patterns.txt" + data_file.write_text("(a+)+$\n") + + rules_dir = tmp_path / "rules" + rules_dir.mkdir() + (rules_dir / "regex.yaml").write_text( + """ +- id: REG001 + name: nested_quantifiers + analyzer: regex + severity: critical + description: "Nested quantifiers" + matcher: "nested_quantifiers == true" + suggestion: "Rewrite the pattern." +""" + ) + + result = scan_target(ScanTarget(path=data_file, analyzer="regex", rules_dir=rules_dir)) + assert result.score.total_findings == 1 + assert result.findings[0].rule_id == "REG001" + + +def test_serialize_finding_shape(): + findings = scan_target( + ScanTarget( + path=Path(__file__).resolve().parent.parent / "samples" / "secrets", + analyzer="secrets", + rules_dir=Path(__file__).resolve().parent.parent / "rules", + ) + ).findings + payload = serialize_finding(findings[0]) + assert payload["rule_id"] + assert payload["severity"] diff --git a/tests/test_secrets_analyzer.py b/tests/test_secrets_analyzer.py index 57a52e3..172c9a2 100644 --- a/tests/test_secrets_analyzer.py +++ b/tests/test_secrets_analyzer.py @@ -11,20 +11,34 @@ RULES_DIR = Path(__file__).resolve().parent.parent / "rules" +def fake_aws_access_key() -> str: + return "AKIA" + "IOSFODNN7EXAMPLE" + + +def fake_github_token() -> str: + return "ghp_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh" + + +def fake_private_key_block() -> str: + begin = "-----BEGIN " + "RSA PRIVATE KEY-----" + end = "-----END " + "RSA PRIVATE KEY-----" + return f"{begin}\nMIIEp...\n{end}\n" + + class TestExtractFacts: def test_detects_aws_access_key(self): - content = "key: AKIAIOSFODNN7EXAMPLE \n" + content = f"key: {fake_aws_access_key()} \n" facts = extract_facts(content) assert facts["has_aws_key"] is True assert facts["secret_count"] >= 1 def test_detects_github_token(self): - content = "GITHUB_TOKEN=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh\n" + content = f"GITHUB_TOKEN={fake_github_token()}\n" facts = extract_facts(content) assert facts["secret_count"] >= 1 def test_detects_private_key(self): - content = "-----BEGIN RSA PRIVATE KEY-----\nMIIEp...\n-----END RSA PRIVATE KEY-----\n" + content = fake_private_key_block() facts = extract_facts(content) assert facts["has_private_key"] is True @@ -49,7 +63,7 @@ def test_clean_file_no_secrets(self): assert facts["secret_count"] == 0 def test_redacted_preview(self): - content = "GITHUB_TOKEN=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh\n" + content = f"GITHUB_TOKEN={fake_github_token()}\n" facts = extract_facts(content) for _, _, preview in facts["secrets_found"]: assert "****" in preview @@ -58,7 +72,7 @@ def test_redacted_preview(self): class TestAnalyze: def test_analyze_secrets_file(self): with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as f: - f.write("AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE\n") + f.write(f"AWS_ACCESS_KEY_ID={fake_aws_access_key()}\n") f.write("DB_PASSWORD=admin123\n") path = Path(f.name) try: @@ -87,7 +101,7 @@ def test_analyze_directory(self, tmp_path): def test_findings_have_attack_vector(self): with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as f: - f.write("GITHUB_TOKEN=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh\n") + f.write(f"GITHUB_TOKEN={fake_github_token()}\n") path = Path(f.name) try: findings = analyze(path, RULES_DIR) @@ -97,7 +111,7 @@ def test_findings_have_attack_vector(self): def test_findings_have_cwe(self): with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as f: - f.write("AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE\n") + f.write(f"AWS_ACCESS_KEY_ID={fake_aws_access_key()}\n") path = Path(f.name) try: findings = analyze(path, RULES_DIR) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..13b0a5e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "rootDir": "vscode-src", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "moduleResolution": "node", + "skipLibCheck": true, + "sourceMap": true + }, + "include": ["vscode-src/**/*.ts"] +} diff --git a/vscode-src/extension.ts b/vscode-src/extension.ts new file mode 100644 index 0000000..0d73f58 --- /dev/null +++ b/vscode-src/extension.ts @@ -0,0 +1,373 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; + +import { FindingsTreeDataProvider } from './findingsTree'; +import { installPythonRuntime, runContractGuardScan } from './pythonBridge'; +import { Finding, ScanPayload, Severity } from './types'; + +const sourceName = 'ContractGuard'; +const supportedExtensions = new Set([ + '.json', + '.sql', + '.txt', + '.regex', + '.env', + '.yaml', + '.yml', + '.toml', + '.ini', + '.cfg', + '.conf', + '.properties', + '.dockerfile' +]); + +class ContractGuardController implements vscode.Disposable { + private readonly diagnostics = vscode.languages.createDiagnosticCollection('contractguard'); + private readonly tree = new FindingsTreeDataProvider(); + private readonly statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50); + private scanTimer: NodeJS.Timeout | undefined; + private running = false; + private queuedScan: (() => void) | undefined; + private latestPayload: ScanPayload | undefined; + + constructor(private readonly context: vscode.ExtensionContext) { + this.statusBar.name = 'ContractGuard'; + this.statusBar.command = 'contractguard.scanWorkspace'; + this.statusBar.text = 'ContractGuard: idle'; + this.statusBar.show(); + + context.subscriptions.push( + this.diagnostics, + this.statusBar, + vscode.window.registerTreeDataProvider('contractguard.findings', this.tree) + ); + } + + dispose(): void { + if (this.scanTimer) { + clearTimeout(this.scanTimer); + } + this.diagnostics.dispose(); + this.statusBar.dispose(); + } + + async scanWorkspace(includeSarif = false): Promise { + const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!workspacePath) { + vscode.window.showInformationMessage('ContractGuard requires an open workspace.'); + return; + } + await this.runScan(workspacePath, 'all', includeSarif); + } + + async scanCurrentFile(): Promise { + const document = vscode.window.activeTextEditor?.document; + if (!document) { + vscode.window.showInformationMessage('No active file to scan.'); + return; + } + await this.runScan(document.uri.fsPath, this.selectAnalyzer(document.uri.fsPath), false); + } + + clear(): void { + this.latestPayload = undefined; + this.tree.clear(); + this.diagnostics.clear(); + this.statusBar.text = 'ContractGuard: cleared'; + } + + async exportSarif(): Promise { + const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!workspacePath) { + vscode.window.showInformationMessage('ContractGuard requires an open workspace.'); + return; + } + + const payload = this.latestPayload?.sarif ? this.latestPayload : await this.collectWorkspaceSarif(workspacePath); + if (!payload.sarif) { + vscode.window.showWarningMessage('ContractGuard did not return SARIF data.'); + return; + } + + const target = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(path.join(workspacePath, 'contractguard.sarif')), + filters: { SARIF: ['sarif', 'json'] } + }); + if (!target) { + return; + } + + fs.writeFileSync(target.fsPath, JSON.stringify(payload.sarif, null, 2), 'utf8'); + vscode.window.showInformationMessage(`ContractGuard SARIF exported to ${target.fsPath}`); + } + + scheduleWorkspaceScan(): void { + const debounceMs = vscode.workspace.getConfiguration('contractguard').get('scanDebounceMs', 600); + if (this.scanTimer) { + clearTimeout(this.scanTimer); + } + this.scanTimer = setTimeout(() => { + void this.scanWorkspace(false); + }, debounceMs); + } + + async openFinding(finding: Finding): Promise { + const parsed = this.parseLocation(finding.location); + if (!parsed) { + return; + } + + const document = await vscode.workspace.openTextDocument(parsed.uri); + const editor = await vscode.window.showTextDocument(document, { preview: false }); + const position = new vscode.Position(Math.max(parsed.line - 1, 0), 0); + editor.selection = new vscode.Selection(position, position); + editor.revealRange(new vscode.Range(position, position), vscode.TextEditorRevealType.InCenter); + } + + private async collectWorkspaceSarif(workspacePath: string): Promise { + return await this.runScan(workspacePath, 'all', true); + } + + private async runScan(targetPath: string, analyzer: string, includeSarif: boolean): Promise { + if (this.running) { + return await new Promise((resolve) => { + this.queuedScan = () => { + void this.runScan(targetPath, analyzer, includeSarif).then(resolve); + }; + }); + } + + this.running = true; + this.statusBar.text = 'ContractGuard: scanning...'; + + try { + const payload = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: `ContractGuard scanning ${path.basename(targetPath) || targetPath}` + }, + async () => await runContractGuardScan(this.context, targetPath, analyzer, includeSarif) + ); + + const filteredFindings = this.filterFindings(payload.findings); + const normalizedPayload: ScanPayload = { + ...payload, + findings: filteredFindings, + score: this.recomputeScore(payload.score, filteredFindings) + }; + this.latestPayload = normalizedPayload; + this.publishDiagnostics(filteredFindings); + this.tree.setFindings(filteredFindings); + this.updateStatusBar(normalizedPayload); + return normalizedPayload; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.statusBar.text = 'ContractGuard: runtime error'; + const action = await vscode.window.showErrorMessage(`ContractGuard scan failed: ${message}`, 'Install Runtime'); + if (action === 'Install Runtime') { + await installPythonRuntime(this.context); + } + throw error; + } finally { + this.running = false; + const queued = this.queuedScan; + this.queuedScan = undefined; + if (queued) { + queued(); + } + } + } + + private filterFindings(findings: Finding[]): Finding[] { + const disabledRules = new Set( + vscode.workspace.getConfiguration('contractguard').get('disabledRules', []).map((item) => item.trim()) + ); + const enabledAnalyzers = new Set( + vscode.workspace.getConfiguration('contractguard').get('enabledAnalyzers', []) + ); + + return findings.filter((finding) => { + if (disabledRules.has(finding.rule_id)) { + return false; + } + if (enabledAnalyzers.size === 0) { + return true; + } + const prefix = this.inferAnalyzerFromRule(finding.rule_id); + return enabledAnalyzers.has(prefix); + }); + } + + private recomputeScore(score: ScanPayload['score'], findings: Finding[]): ScanPayload['score'] { + const counts = { + block_count: findings.filter((item) => item.severity === 'block').length, + critical_count: findings.filter((item) => item.severity === 'critical').length, + warning_count: findings.filter((item) => item.severity === 'warning').length, + info_count: findings.filter((item) => item.severity === 'info').length + }; + return { + ...score, + ...counts, + total_findings: findings.length + }; + } + + private updateStatusBar(payload: ScanPayload): void { + this.statusBar.text = `ContractGuard ${payload.score.grade} ${payload.score.score}/100`; + this.statusBar.tooltip = `${payload.score.total_findings} findings`; + switch (payload.score.grade) { + case 'A': + case 'B': + this.statusBar.backgroundColor = undefined; + break; + case 'C': + this.statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); + break; + default: + this.statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground'); + break; + } + } + + private publishDiagnostics(findings: Finding[]): void { + this.diagnostics.clear(); + const buckets = new Map(); + + for (const finding of findings) { + const parsed = this.parseLocation(finding.location); + if (!parsed) { + continue; + } + + const range = new vscode.Range( + new vscode.Position(Math.max(parsed.line - 1, 0), 0), + new vscode.Position(Math.max(parsed.line - 1, 0), 200) + ); + const diagnostic = new vscode.Diagnostic(range, this.formatMessage(finding), this.toDiagnosticSeverity(finding.severity)); + diagnostic.code = finding.rule_id; + diagnostic.source = sourceName; + + const items = buckets.get(parsed.uri.fsPath) ?? []; + items.push(diagnostic); + buckets.set(parsed.uri.fsPath, items); + } + + for (const [filePath, diagnostics] of buckets.entries()) { + this.diagnostics.set(vscode.Uri.file(filePath), diagnostics); + } + } + + private formatMessage(finding: Finding): string { + const bits = [finding.description, finding.suggestion]; + if (finding.cwe) { + bits.push(finding.cwe); + } + return bits.filter(Boolean).join(' '); + } + + private toDiagnosticSeverity(severity: Severity): vscode.DiagnosticSeverity { + switch (severity) { + case 'block': + case 'critical': + return vscode.DiagnosticSeverity.Error; + case 'warning': + return vscode.DiagnosticSeverity.Warning; + default: + return vscode.DiagnosticSeverity.Information; + } + } + + private inferAnalyzerFromRule(ruleId: string): string { + if (ruleId.startsWith('JSON')) return 'json'; + if (ruleId.startsWith('SQL')) return 'sql'; + if (ruleId.startsWith('REG')) return 'regex'; + if (ruleId.startsWith('SEC')) return 'secrets'; + if (ruleId.startsWith('PII')) return 'pii'; + if (ruleId.startsWith('CFG')) return 'config'; + if (ruleId.startsWith('DOCK')) return 'dockerfile'; + if (ruleId.startsWith('DEP') || ruleId.startsWith('CVE')) return 'deps'; + return 'secrets'; + } + + private parseLocation(location: string): { uri: vscode.Uri; line: number } | undefined { + if (!location) { + return undefined; + } + + const parts = location.match(/^(.*?)(?::(\d+))?$/); + if (!parts) { + return undefined; + } + const filePath = parts[1]; + if (!filePath || !fs.existsSync(filePath)) { + return undefined; + } + return { + uri: vscode.Uri.file(filePath), + line: parts[2] ? Number(parts[2]) : 1 + }; + } + + private selectAnalyzer(filePath: string): string { + const extension = path.extname(filePath).toLowerCase(); + const basename = path.basename(filePath).toLowerCase(); + + if (extension === '.sql') return 'sql'; + if (extension === '.json') return 'json'; + if (extension === '.regex') return 'regex'; + if (basename === 'dockerfile' || extension === '.dockerfile') return 'dockerfile'; + if (basename.startsWith('requirements') || basename === 'constraints.txt') return 'deps'; + if (extension === '.env' || extension === '.yaml' || extension === '.yml' || extension === '.toml' || extension === '.ini' || extension === '.cfg' || extension === '.conf' || extension === '.properties') { + return 'config'; + } + if (supportedExtensions.has(extension)) { + return 'secrets'; + } + return 'all'; + } +} + +export function activate(context: vscode.ExtensionContext): void { + const controller = new ContractGuardController(context); + + context.subscriptions.push( + controller, + vscode.commands.registerCommand('contractguard.scanWorkspace', async () => { + await controller.scanWorkspace(false); + }), + vscode.commands.registerCommand('contractguard.scanCurrentFile', async () => { + await controller.scanCurrentFile(); + }), + vscode.commands.registerCommand('contractguard.exportSarif', async () => { + await controller.exportSarif(); + }), + vscode.commands.registerCommand('contractguard.clearFindings', () => { + controller.clear(); + }), + vscode.commands.registerCommand('contractguard.openFinding', async (finding: Finding) => { + await controller.openFinding(finding); + }), + vscode.commands.registerCommand('contractguard.installRuntime', async () => { + await installPythonRuntime(context); + vscode.window.showInformationMessage('ContractGuard Python runtime dependencies installed.'); + }), + vscode.workspace.onDidSaveTextDocument((document) => { + if (!vscode.workspace.getConfiguration('contractguard').get('scanOnSave', true)) { + return; + } + if (document.uri.scheme !== 'file') { + return; + } + controller.scheduleWorkspaceScan(); + }), + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration('contractguard')) { + controller.scheduleWorkspaceScan(); + } + }) + ); +} + +export function deactivate(): void {} diff --git a/vscode-src/findingsTree.ts b/vscode-src/findingsTree.ts new file mode 100644 index 0000000..8bd4aba --- /dev/null +++ b/vscode-src/findingsTree.ts @@ -0,0 +1,86 @@ +import * as path from 'path'; +import * as vscode from 'vscode'; + +import { Finding, Severity } from './types'; + +type TreeNode = SeverityGroupNode | FindingNode; + +const severityOrder: Severity[] = ['block', 'critical', 'warning', 'info']; + +function severityIcon(severity: Severity): vscode.ThemeIcon { + switch (severity) { + case 'block': + return new vscode.ThemeIcon('error', new vscode.ThemeColor('problemsErrorIcon.foreground')); + case 'critical': + return new vscode.ThemeIcon('warning', new vscode.ThemeColor('problemsErrorIcon.foreground')); + case 'warning': + return new vscode.ThemeIcon('warning', new vscode.ThemeColor('problemsWarningIcon.foreground')); + default: + return new vscode.ThemeIcon('info', new vscode.ThemeColor('problemsInfoIcon.foreground')); + } +} + +class SeverityGroupNode extends vscode.TreeItem { + constructor( + public readonly severity: Severity, + public readonly findings: Finding[] + ) { + super(`${severity.toUpperCase()} (${findings.length})`, vscode.TreeItemCollapsibleState.Expanded); + this.iconPath = severityIcon(severity); + this.contextValue = 'severity-group'; + } +} + +class FindingNode extends vscode.TreeItem { + constructor(public readonly finding: Finding) { + const basename = finding.location ? path.basename(finding.location.split(':')[0]) : finding.rule_id; + super(`${finding.rule_id} ${basename}`, vscode.TreeItemCollapsibleState.None); + this.description = finding.description; + this.tooltip = new vscode.MarkdownString( + `**${finding.rule_id}**\n\n${finding.description}\n\n${finding.suggestion}\n\n${finding.location || 'workspace'}` + ); + this.iconPath = severityIcon(finding.severity); + this.command = { + command: 'contractguard.openFinding', + title: 'Open Finding', + arguments: [finding] + }; + this.contextValue = 'finding'; + } +} + +export class FindingsTreeDataProvider implements vscode.TreeDataProvider { + private readonly emitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this.emitter.event; + private findings: Finding[] = []; + + setFindings(findings: Finding[]): void { + this.findings = findings; + this.emitter.fire(undefined); + } + + clear(): void { + this.setFindings([]); + } + + getTreeItem(element: TreeNode): vscode.TreeItem { + return element; + } + + getChildren(element?: TreeNode): TreeNode[] { + if (!element) { + return severityOrder + .map((severity) => { + const items = this.findings.filter((finding) => finding.severity === severity); + return items.length > 0 ? new SeverityGroupNode(severity, items) : undefined; + }) + .filter((node): node is SeverityGroupNode => Boolean(node)); + } + + if (element instanceof SeverityGroupNode) { + return element.findings.map((finding) => new FindingNode(finding)); + } + + return []; + } +} diff --git a/vscode-src/pythonBridge.ts b/vscode-src/pythonBridge.ts new file mode 100644 index 0000000..f58f9ed --- /dev/null +++ b/vscode-src/pythonBridge.ts @@ -0,0 +1,154 @@ +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; + +import { ScanPayload } from './types'; + +function getConfig(): vscode.WorkspaceConfiguration { + return vscode.workspace.getConfiguration('contractguard'); +} + +function getWorkspaceRoot(): string | undefined { + return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; +} + +function getBundledRulesPath(context: vscode.ExtensionContext): string { + const configured = getConfig().get('rulesDirectory', '').trim(); + return configured ? configured : path.join(context.extensionPath, 'rules'); +} + +function getPythonExecutable(): string { + const configured = getConfig().get('pythonPath', '').trim(); + if (configured) { + return configured; + } + + const workspaceRoot = getWorkspaceRoot(); + if (workspaceRoot) { + const candidates = [ + path.join(workspaceRoot, '.venv', 'Scripts', 'python.exe'), + path.join(workspaceRoot, '.venv', 'bin', 'python'), + path.join(workspaceRoot, 'venv', 'Scripts', 'python.exe'), + path.join(workspaceRoot, 'venv', 'bin', 'python') + ]; + const match = candidates.find((candidate) => fs.existsSync(candidate)); + if (match) { + return match; + } + } + + return process.platform === 'win32' ? 'python' : 'python3'; +} + +function getPythonPathEntries(context: vscode.ExtensionContext): string[] { + const bundledSrc = path.join(context.extensionPath, 'src'); + const entries = [bundledSrc]; + const existing = process.env.PYTHONPATH?.trim(); + if (existing) { + entries.push(existing); + } + return entries; +} + +export async function runContractGuardScan( + context: vscode.ExtensionContext, + targetPath: string, + analyzer: string, + includeSarif: boolean +): Promise { + const python = getPythonExecutable(); + const dbPath = getConfig().get('sqlExplainDatabase', '').trim(); + const args = [ + '-m', + 'contractguard.bridge', + 'scan', + '--path', + targetPath, + '--analyzer', + analyzer, + '--rules-dir', + getBundledRulesPath(context) + ]; + + if (dbPath) { + args.push('--db', dbPath); + } + if (includeSarif) { + args.push('--include-sarif'); + } + + const env = { + ...process.env, + PYTHONPATH: getPythonPathEntries(context).join(path.delimiter) + }; + + return await new Promise((resolve, reject) => { + const child = cp.spawn(python, args, { + cwd: getWorkspaceRoot() ?? context.extensionPath, + env, + windowsHide: true + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (chunk: Buffer | string) => { + stdout += chunk.toString(); + }); + child.stderr.on('data', (chunk: Buffer | string) => { + stderr += chunk.toString(); + }); + + child.on('error', (error) => { + reject(error); + }); + + child.on('close', (code) => { + if (code !== 0) { + reject(new Error(stderr.trim() || `ContractGuard bridge exited with code ${code}`)); + return; + } + + try { + resolve(JSON.parse(stdout) as ScanPayload); + } catch (error) { + const detail = error instanceof Error ? error.message : String(error); + reject(new Error(`Failed to parse ContractGuard output: ${detail}\n${stdout}`)); + } + }); + }); +} + +export async function installPythonRuntime(context: vscode.ExtensionContext): Promise { + const python = getPythonExecutable(); + const requirementsFile = path.join(context.extensionPath, 'python-requirements.txt'); + const args = ['-m', 'pip', 'install', '-r', requirementsFile]; + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Installing ContractGuard Python dependencies' + }, + async () => + await new Promise((resolve, reject) => { + const child = cp.spawn(python, args, { + cwd: getWorkspaceRoot() ?? context.extensionPath, + windowsHide: true + }); + + let stderr = ''; + child.stderr.on('data', (chunk: Buffer | string) => { + stderr += chunk.toString(); + }); + child.on('error', reject); + child.on('close', (code) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(stderr.trim() || `pip exited with code ${code}`)); + }); + }) + ); +} diff --git a/vscode-src/types.ts b/vscode-src/types.ts new file mode 100644 index 0000000..e2eccde --- /dev/null +++ b/vscode-src/types.ts @@ -0,0 +1,38 @@ +export type Severity = 'info' | 'warning' | 'critical' | 'block'; + +export interface Finding { + rule_id: string; + rule_name: string; + severity: Severity; + description: string; + explanation: string; + suggestion: string; + location: string; + context: string; + attack_vector: string; + cwe: string; + confidence: string; +} + +export interface ScoreSummary { + grade: string; + score: number; + total_findings: number; + block_count: number; + critical_count: number; + warning_count: number; + info_count: number; + risk_summary: string; + attack_surface: string[]; + top_risks: string[]; +} + +export interface ScanPayload { + target: string; + analyzer: string; + engine_version: string; + generated_at: string | null; + findings: Finding[]; + score: ScoreSummary; + sarif: Record | null; +} From 5e56d9f890ca275fbf7af94b3796156eba14aec0 Mon Sep 17 00:00:00 2001 From: Brahadeesh V Ra <144269250+CodeforGood1@users.noreply.github.com> Date: Sun, 10 May 2026 01:22:22 +0530 Subject: [PATCH 2/4] Fix VSIX packaging in CI and align version to 1.0.0 --- .github/workflows/contractguard-ci.yml | 3 +++ package-lock.json | 4 ++-- package.json | 4 ++-- pyproject.toml | 2 +- src/contractguard/__init__.py | 2 +- src/contractguard/reporter.py | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/contractguard-ci.yml b/.github/workflows/contractguard-ci.yml index 52177d9..2ecc951 100644 --- a/.github/workflows/contractguard-ci.yml +++ b/.github/workflows/contractguard-ci.yml @@ -44,6 +44,9 @@ jobs: - name: Build extension run: npm run build + - name: Create VSIX output directory + run: mkdir -p dist-vsix + - name: Package VSIX run: npm run package diff --git a/package-lock.json b/package-lock.json index 46574dd..d44c5c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "contractguard", - "version": "3.0.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "contractguard", - "version": "3.0.0", + "version": "1.0.0", "license": "Apache-2.0", "devDependencies": { "@types/node": "^20.16.1", diff --git a/package.json b/package.json index ee303e2..578a1e1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "contractguard", "displayName": "ContractGuard", "description": "Security analysis for code, config, Dockerfiles, data payloads, and dependencies.", - "version": "3.0.0", + "version": "1.0.0", "publisher": "BlackplaneSystems", "license": "Apache-2.0", "icon": "media/icon.png", @@ -158,7 +158,7 @@ }, "scripts": { "build": "tsc -p ./tsconfig.json", - "package": "vsce package --out dist-vsix/contractguard-3.0.0.vsix", + "package": "node -e \"require('fs').mkdirSync('dist-vsix',{recursive:true})\" && vsce package --out dist-vsix/contractguard-1.0.0.vsix", "prepackage": "npm run build" }, "devDependencies": { diff --git a/pyproject.toml b/pyproject.toml index 4458b61..a845ba7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "contractguard" -version = "3.0.0" +version = "1.0.0" description = "ContractGuard security analysis core for VS Code and CI workflows." readme = "README.md" license = {text = "Apache-2.0"} diff --git a/src/contractguard/__init__.py b/src/contractguard/__init__.py index 33bcc5a..1645bc6 100644 --- a/src/contractguard/__init__.py +++ b/src/contractguard/__init__.py @@ -1,3 +1,3 @@ """ContractGuard core package.""" -__version__ = "3.0.0" +__version__ = "1.0.0" diff --git a/src/contractguard/reporter.py b/src/contractguard/reporter.py index 0b9d31b..4c3fa7c 100644 --- a/src/contractguard/reporter.py +++ b/src/contractguard/reporter.py @@ -209,7 +209,7 @@ def render_sarif_report( "tool": { "driver": { "name": "ContractGuard", - "version": "3.0.0", + "version": "1.0.0", "informationUri": "https://github.com/contractguard/contractguard", "rules": rules, } From 7cff18f67c49bbd635568f15e167d2fb0f3b8245 Mon Sep 17 00:00:00 2001 From: Brahadeesh V Ra <144269250+CodeforGood1@users.noreply.github.com> Date: Sun, 10 May 2026 22:59:20 +0530 Subject: [PATCH 3/4] Release contract-guard 1.1.0 --- README.md | 77 ++++++--------- package-lock.json | 4 +- package.json | 8 +- pyproject.toml | 2 +- python-requirements.txt | 3 - src/contractguard/__init__.py | 2 +- src/contractguard/reporter.py | 4 +- vscode-src/extension.ts | 172 +++++++++++++++++++++++++++++----- 8 files changed, 188 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 742b42d..3b3aab1 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,44 @@ -# ContractGuard for VS Code +# contract-guard -ContractGuard is a VS Code extension backed by a Python security analysis core. It scans source trees for schema drift, risky SQL, regex complexity, secrets, PII, insecure configuration, Dockerfile issues, and vulnerable dependencies, then surfaces the results as diagnostics, a findings explorer, a status bar score, and SARIF exports. +`contract-guard` helps you find security and reliability issues in code, configs, queries, Dockerfiles, and dependency files without leaving VS Code. -## What ships in this repository +## Features -- A reusable Python engine in `src/contractguard` with rule-driven analyzers, scoring, findings, history, and SARIF generation. -- A VS Code extension in `vscode-src` that runs the engine in a separate Python process and renders results inside the editor. -- Rules in `rules/` that stay bundled with the extension and CLI. +- Scan the current file +- Scan the full workspace +- Show findings in a dedicated explorer view +- Publish inline diagnostics in the editor +- Export SARIF for external security workflows +- Show an overall security score in the status bar -## Supported analyzers +## What it checks -- JSON schema analysis -- SQL analysis -- Regex complexity analysis -- Secrets detection -- PII detection -- Config security analysis -- Dockerfile linting -- Dependency vulnerability analysis +- JSON schema inconsistencies +- SQL query risks and anti-patterns +- Regex complexity and ReDoS risks +- Hardcoded secrets +- PII exposure +- Insecure configuration +- Dockerfile issues +- Dependency vulnerabilities -## VS Code features +## Commands - `ContractGuard: Scan Workspace` - `ContractGuard: Scan Current File` - `ContractGuard: Export SARIF` - `ContractGuard: Clear Findings` -- Findings tree view grouped by severity -- Inline diagnostics and quick navigation -- Status bar security grade -- Debounced scan-on-save -- Configurable analyzer set and disabled rules +- `ContractGuard: Install Python Runtime Dependencies` -## Runtime requirements +## Requirements -- Python 3.11+ available on the machine running VS Code -- Python packages from `python-requirements.txt` +- Python 3.11 or newer -For local development in this repository: +If the Python runtime dependencies are missing, run: -```powershell -python -m venv .venv -.\.venv\Scripts\Activate.ps1 -pip install -r python-requirements.txt -``` +- `ContractGuard: Install Python Runtime Dependencies` -## Development commands - -Python: - -```powershell -.\.venv\Scripts\python.exe -m pytest -.\.venv\Scripts\python.exe -m contractguard.bridge scan --path . --analyzer all --include-sarif -``` - -Extension: - -```powershell -node .\node_modules\typescript\bin\tsc -p .\tsconfig.json -node .\node_modules\@vscode\vsce\vsce package -``` - -## Settings +## Extension Settings - `contractguard.pythonPath` - `contractguard.scanOnSave` @@ -70,6 +48,7 @@ node .\node_modules\@vscode\vsce\vsce package - `contractguard.rulesDirectory` - `contractguard.sqlExplainDatabase` -## Packaging +## Notes -The extension is packaged from the repository root. The VSIX includes the compiled extension, bundled Python source, rules, and documentation. The output artifact is written to `dist-vsix/`. +- The extension runs analysis locally. +- SARIF export is available for CI and external security tooling. diff --git a/package-lock.json b/package-lock.json index d44c5c1..f87d8bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "contractguard", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "contractguard", - "version": "1.0.0", + "version": "1.1.0", "license": "Apache-2.0", "devDependencies": { "@types/node": "^20.16.1", diff --git a/package.json b/package.json index 578a1e1..3096189 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "contractguard", - "displayName": "ContractGuard", + "name": "contract-guard", + "displayName": "contract-guard", "description": "Security analysis for code, config, Dockerfiles, data payloads, and dependencies.", - "version": "1.0.0", + "version": "1.1.0", "publisher": "BlackplaneSystems", "license": "Apache-2.0", "icon": "media/icon.png", @@ -158,7 +158,7 @@ }, "scripts": { "build": "tsc -p ./tsconfig.json", - "package": "node -e \"require('fs').mkdirSync('dist-vsix',{recursive:true})\" && vsce package --out dist-vsix/contractguard-1.0.0.vsix", + "package": "node -e \"require('fs').mkdirSync('dist-vsix',{recursive:true})\" && vsce package --out dist-vsix/contractguard-1.1.0.vsix", "prepackage": "npm run build" }, "devDependencies": { diff --git a/pyproject.toml b/pyproject.toml index a845ba7..00a2766 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "contractguard" -version = "1.0.0" +version = "1.1.0" description = "ContractGuard security analysis core for VS Code and CI workflows." readme = "README.md" license = {text = "Apache-2.0"} diff --git a/python-requirements.txt b/python-requirements.txt index e34cde3..5a7c49b 100644 --- a/python-requirements.txt +++ b/python-requirements.txt @@ -4,6 +4,3 @@ pyyaml>=6.0 jsonschema>=4.20.0 sqlparse>=0.5.0 jinja2>=3.1.0 -pytest>=7.4.0 -pytest-cov>=4.1.0 -httpx>=0.25.0 diff --git a/src/contractguard/__init__.py b/src/contractguard/__init__.py index 1645bc6..91e3269 100644 --- a/src/contractguard/__init__.py +++ b/src/contractguard/__init__.py @@ -1,3 +1,3 @@ """ContractGuard core package.""" -__version__ = "1.0.0" +__version__ = "1.1.0" diff --git a/src/contractguard/reporter.py b/src/contractguard/reporter.py index 4c3fa7c..7ba3719 100644 --- a/src/contractguard/reporter.py +++ b/src/contractguard/reporter.py @@ -209,8 +209,8 @@ def render_sarif_report( "tool": { "driver": { "name": "ContractGuard", - "version": "1.0.0", - "informationUri": "https://github.com/contractguard/contractguard", + "version": "1.1.0", + "informationUri": "https://github.com/Blackplane-Systems/contractguard", "rules": rules, } }, diff --git a/vscode-src/extension.ts b/vscode-src/extension.ts index 0d73f58..3d175c0 100644 --- a/vscode-src/extension.ts +++ b/vscode-src/extension.ts @@ -7,6 +7,7 @@ import { installPythonRuntime, runContractGuardScan } from './pythonBridge'; import { Finding, ScanPayload, Severity } from './types'; const sourceName = 'ContractGuard'; +const analyzerIds = ['json', 'sql', 'regex', 'secrets', 'pii', 'config', 'dockerfile', 'deps'] as const; const supportedExtensions = new Set([ '.json', '.sql', @@ -29,7 +30,13 @@ class ContractGuardController implements vscode.Disposable { private readonly statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50); private scanTimer: NodeJS.Timeout | undefined; private running = false; - private queuedScan: (() => void) | undefined; + private queuedRequest: + | { targetPath: string; analyzer: string; includeSarif: boolean } + | undefined; + private queuedScanPromise: Promise | undefined; + private resolveQueuedScan: ((payload: ScanPayload) => void) | undefined; + private rejectQueuedScan: ((error: unknown) => void) | undefined; + private scheduledScanAction: (() => Promise) | undefined; private latestPayload: ScanPayload | undefined; constructor(private readonly context: vscode.ExtensionContext) { @@ -59,7 +66,7 @@ class ContractGuardController implements vscode.Disposable { vscode.window.showInformationMessage('ContractGuard requires an open workspace.'); return; } - await this.runScan(workspacePath, 'all', includeSarif); + await this.runWorkspaceScan(workspacePath, includeSarif); } async scanCurrentFile(): Promise { @@ -68,7 +75,13 @@ class ContractGuardController implements vscode.Disposable { vscode.window.showInformationMessage('No active file to scan.'); return; } - await this.runScan(document.uri.fsPath, this.selectAnalyzer(document.uri.fsPath), false); + const filePath = document.uri.fsPath; + const analyzer = this.selectAnalyzer(filePath); + if (analyzer === 'all') { + vscode.window.showInformationMessage(`ContractGuard does not support scanning this file type: ${path.basename(filePath)}`); + return; + } + await this.runScan(filePath, analyzer, false); } clear(): void { @@ -99,18 +112,25 @@ class ContractGuardController implements vscode.Disposable { return; } - fs.writeFileSync(target.fsPath, JSON.stringify(payload.sarif, null, 2), 'utf8'); + await vscode.workspace.fs.writeFile(target, new TextEncoder().encode(JSON.stringify(payload.sarif, null, 2))); vscode.window.showInformationMessage(`ContractGuard SARIF exported to ${target.fsPath}`); } scheduleWorkspaceScan(): void { - const debounceMs = vscode.workspace.getConfiguration('contractguard').get('scanDebounceMs', 600); - if (this.scanTimer) { - clearTimeout(this.scanTimer); + this.scheduleScan(async () => { + await this.scanWorkspace(false); + }); + } + + scheduleFileScan(filePath: string): void { + const analyzer = this.selectAnalyzer(filePath); + if (analyzer === 'all') { + return; } - this.scanTimer = setTimeout(() => { - void this.scanWorkspace(false); - }, debounceMs); + + this.scheduleScan(async () => { + await this.runScan(filePath, analyzer, false); + }); } async openFinding(finding: Finding): Promise { @@ -127,16 +147,30 @@ class ContractGuardController implements vscode.Disposable { } private async collectWorkspaceSarif(workspacePath: string): Promise { - return await this.runScan(workspacePath, 'all', true); + return await this.runWorkspaceScan(workspacePath, true); + } + + private async runWorkspaceScan(workspacePath: string, includeSarif: boolean): Promise { + const analyzers = this.getEnabledAnalyzers(); + const payloads: ScanPayload[] = []; + + for (const analyzer of analyzers) { + payloads.push(await this.runScan(workspacePath, analyzer, includeSarif)); + } + + return this.mergePayloads(workspacePath, payloads, includeSarif); } private async runScan(targetPath: string, analyzer: string, includeSarif: boolean): Promise { if (this.running) { - return await new Promise((resolve) => { - this.queuedScan = () => { - void this.runScan(targetPath, analyzer, includeSarif).then(resolve); - }; - }); + this.queuedRequest = { targetPath, analyzer, includeSarif }; + if (!this.queuedScanPromise) { + this.queuedScanPromise = new Promise((resolve, reject) => { + this.resolveQueuedScan = resolve; + this.rejectQueuedScan = reject; + }); + } + return await this.queuedScanPromise; } this.running = true; @@ -172,14 +206,90 @@ class ContractGuardController implements vscode.Disposable { throw error; } finally { this.running = false; - const queued = this.queuedScan; - this.queuedScan = undefined; - if (queued) { - queued(); + const queuedRequest = this.queuedRequest; + const resolveQueuedScan = this.resolveQueuedScan; + const rejectQueuedScan = this.rejectQueuedScan; + this.queuedRequest = undefined; + this.queuedScanPromise = undefined; + this.resolveQueuedScan = undefined; + this.rejectQueuedScan = undefined; + if (queuedRequest && resolveQueuedScan && rejectQueuedScan) { + void this.runScan(queuedRequest.targetPath, queuedRequest.analyzer, queuedRequest.includeSarif).then( + resolveQueuedScan, + rejectQueuedScan + ); } } } + private scheduleScan(action: () => Promise): void { + const debounceMs = vscode.workspace.getConfiguration('contractguard').get('scanDebounceMs', 600); + this.scheduledScanAction = action; + if (this.scanTimer) { + clearTimeout(this.scanTimer); + } + this.scanTimer = setTimeout(() => { + const scheduledScanAction = this.scheduledScanAction; + this.scheduledScanAction = undefined; + if (scheduledScanAction) { + void scheduledScanAction(); + } + }, debounceMs); + } + + private getEnabledAnalyzers(): string[] { + const configured = vscode.workspace.getConfiguration('contractguard').get('enabledAnalyzers', []); + if (configured.length === 0) { + return [...analyzerIds]; + } + const configuredSet = new Set(configured); + return analyzerIds.filter((analyzer) => configuredSet.has(analyzer)); + } + + private mergePayloads(workspacePath: string, payloads: ScanPayload[], includeSarif: boolean): ScanPayload { + const findings = payloads.flatMap((payload) => payload.findings); + const score = this.recomputeScore( + payloads[0]?.score ?? { + grade: 'A', + score: 100, + total_findings: 0, + block_count: 0, + critical_count: 0, + warning_count: 0, + info_count: 0, + risk_summary: '', + attack_surface: [], + top_risks: [] + }, + findings + ); + const sarif = includeSarif + ? { + version: '2.1.0', + $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json', + runs: payloads.flatMap((payload) => { + const runs = payload.sarif && 'runs' in payload.sarif ? payload.sarif.runs : []; + return Array.isArray(runs) ? runs : []; + }) + } + : null; + + const mergedPayload: ScanPayload = { + target: workspacePath, + analyzer: payloads.length === 1 ? payloads[0].analyzer : 'all', + engine_version: payloads[0]?.engine_version ?? 'unknown', + generated_at: payloads[0]?.generated_at ?? null, + findings, + score, + sarif + }; + this.latestPayload = mergedPayload; + this.publishDiagnostics(findings); + this.tree.setFindings(findings); + this.updateStatusBar(mergedPayload); + return mergedPayload; + } + private filterFindings(findings: Finding[]): Finding[] { const disabledRules = new Set( vscode.workspace.getConfiguration('contractguard').get('disabledRules', []).map((item) => item.trim()) @@ -207,10 +317,28 @@ class ContractGuardController implements vscode.Disposable { warning_count: findings.filter((item) => item.severity === 'warning').length, info_count: findings.filter((item) => item.severity === 'info').length }; + const attackSurface = [...new Set(findings.flatMap((item) => score.attack_surface.includes(item.attack_vector) ? [item.attack_vector] : []))]; + const topRisks = [...new Set(findings.map((item) => `[${item.severity.toUpperCase()}] ${item.description}`))].slice(0, 5); + const scoreValue = Math.max( + 0, + 100 - counts.block_count * 20 - counts.critical_count * 10 - counts.warning_count * 4 - counts.info_count + ); + const grade = + counts.block_count > 0 ? 'F' + : scoreValue >= 90 ? 'A' + : scoreValue >= 75 ? 'B' + : scoreValue >= 55 ? 'C' + : scoreValue >= 35 ? 'D' + : 'F'; + return { ...score, + grade, + score: counts.block_count > 0 ? Math.min(scoreValue, 15) : scoreValue, ...counts, - total_findings: findings.length + total_findings: findings.length, + attack_surface: attackSurface, + top_risks: topRisks }; } @@ -360,7 +488,7 @@ export function activate(context: vscode.ExtensionContext): void { if (document.uri.scheme !== 'file') { return; } - controller.scheduleWorkspaceScan(); + controller.scheduleFileScan(document.uri.fsPath); }), vscode.workspace.onDidChangeConfiguration((event) => { if (event.affectsConfiguration('contractguard')) { From 61c025841356c1d7b10efdba37134a8add9a3955 Mon Sep 17 00:00:00 2001 From: Brahadeesh V Ra <144269250+CodeforGood1@users.noreply.github.com> Date: Sun, 10 May 2026 23:33:28 +0530 Subject: [PATCH 4/4] Release contract-guard 1.2.0 --- package-lock.json | 8 ++++---- package.json | 4 ++-- pyproject.toml | 2 +- src/contractguard/__init__.py | 2 +- src/contractguard/reporter.py | 16 +++++++++------- tests/test_reporter.py | 23 +++++++++++++++++++++-- vscode-src/extension.ts | 24 +++++++++++++++++++++++- vscode-src/findingsTree.ts | 13 ++++++++++++- 8 files changed, 73 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index f87d8bc..81f52c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "contractguard", - "version": "1.1.0", + "name": "contract-guard", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "contractguard", - "version": "1.1.0", + "name": "contract-guard", + "version": "1.2.0", "license": "Apache-2.0", "devDependencies": { "@types/node": "^20.16.1", diff --git a/package.json b/package.json index 3096189..d99d166 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "contract-guard", "displayName": "contract-guard", "description": "Security analysis for code, config, Dockerfiles, data payloads, and dependencies.", - "version": "1.1.0", + "version": "1.2.0", "publisher": "BlackplaneSystems", "license": "Apache-2.0", "icon": "media/icon.png", @@ -158,7 +158,7 @@ }, "scripts": { "build": "tsc -p ./tsconfig.json", - "package": "node -e \"require('fs').mkdirSync('dist-vsix',{recursive:true})\" && vsce package --out dist-vsix/contractguard-1.1.0.vsix", + "package": "node -e \"require('fs').mkdirSync('dist-vsix',{recursive:true})\" && vsce package --out dist-vsix/contractguard-1.2.0.vsix", "prepackage": "npm run build" }, "devDependencies": { diff --git a/pyproject.toml b/pyproject.toml index 00a2766..d7b2f2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "contractguard" -version = "1.1.0" +version = "1.2.0" description = "ContractGuard security analysis core for VS Code and CI workflows." readme = "README.md" license = {text = "Apache-2.0"} diff --git a/src/contractguard/__init__.py b/src/contractguard/__init__.py index 91e3269..0f9bd50 100644 --- a/src/contractguard/__init__.py +++ b/src/contractguard/__init__.py @@ -1,3 +1,3 @@ """ContractGuard core package.""" -__version__ = "1.1.0" +__version__ = "1.2.0" diff --git a/src/contractguard/reporter.py b/src/contractguard/reporter.py index 7ba3719..34b4ed3 100644 --- a/src/contractguard/reporter.py +++ b/src/contractguard/reporter.py @@ -175,14 +175,16 @@ def render_sarif_report( rule_def["fullDescription"] = {"text": f"Attack vector: {finding.attack_vector}"} rules.append(rule_def) - file_path = finding.location.split(":")[0] if finding.location else "" + file_path = finding.location or "" line = 1 - if ":" in finding.location: + if finding.location and ":" in finding.location: parts = finding.location.rsplit(":", 1) - try: - line = int(parts[1]) - except ValueError: - line = 1 + if len(parts) == 2: + try: + line = int(parts[1]) + file_path = parts[0] + except ValueError: + line = 1 result: dict[str, Any] = { "ruleId": finding.rule_id, @@ -209,7 +211,7 @@ def render_sarif_report( "tool": { "driver": { "name": "ContractGuard", - "version": "1.1.0", + "version": "1.2.0", "informationUri": "https://github.com/Blackplane-Systems/contractguard", "rules": rules, } diff --git a/tests/test_reporter.py b/tests/test_reporter.py index 0e78dd1..da76f08 100644 --- a/tests/test_reporter.py +++ b/tests/test_reporter.py @@ -1,7 +1,7 @@ -"""Tests for the HTML reporter.""" +"""Tests for report generation.""" from contractguard.engine import Finding, Severity -from contractguard.reporter import render_html_report +from contractguard.reporter import render_html_report, render_sarif_report class TestRenderHtmlReport: @@ -32,3 +32,22 @@ def test_contains_metadata(self): html = render_html_report([], analyzer_type="regex", source_path="patterns.txt") assert "regex" in html assert "patterns.txt" in html + + def test_sarif_preserves_windows_drive_paths(self): + findings = [ + Finding( + rule_id="TEST002", + rule_name="test", + severity=Severity.WARNING, + description="Needs attention", + explanation="Matched", + suggestion="Fix it", + location=r"C:\repo\.env:12", + context="API_KEY=example", + ) + ] + + sarif = render_sarif_report(findings) + location = sarif["runs"][0]["results"][0]["locations"][0]["physicalLocation"] + assert location["artifactLocation"]["uri"] == "C:/repo/.env" + assert location["region"]["startLine"] == 12 diff --git a/vscode-src/extension.ts b/vscode-src/extension.ts index 3d175c0..338dbb0 100644 --- a/vscode-src/extension.ts +++ b/vscode-src/extension.ts @@ -24,6 +24,21 @@ const supportedExtensions = new Set([ '.dockerfile' ]); +function riskSummaryForGrade(grade: string): string { + switch (grade) { + case 'A': + return 'Minimal risk. Good security posture.'; + case 'B': + return 'Low risk. A few issues to address before production.'; + case 'C': + return 'Moderate risk. Several issues need attention.'; + case 'D': + return 'High risk. Significant security issues detected.'; + default: + return 'CRITICAL RISK. Deployment must be blocked until issues are resolved.'; + } +} + class ContractGuardController implements vscode.Disposable { private readonly diagnostics = vscode.languages.createDiagnosticCollection('contractguard'); private readonly tree = new FindingsTreeDataProvider(); @@ -317,7 +332,13 @@ class ContractGuardController implements vscode.Disposable { warning_count: findings.filter((item) => item.severity === 'warning').length, info_count: findings.filter((item) => item.severity === 'info').length }; - const attackSurface = [...new Set(findings.flatMap((item) => score.attack_surface.includes(item.attack_vector) ? [item.attack_vector] : []))]; + const attackSurface = [ + ...new Set( + findings + .map((item) => item.attack_vector) + .filter((attackVector) => typeof attackVector === 'string' && attackVector.trim().length > 0) + ) + ]; const topRisks = [...new Set(findings.map((item) => `[${item.severity.toUpperCase()}] ${item.description}`))].slice(0, 5); const scoreValue = Math.max( 0, @@ -337,6 +358,7 @@ class ContractGuardController implements vscode.Disposable { score: counts.block_count > 0 ? Math.min(scoreValue, 15) : scoreValue, ...counts, total_findings: findings.length, + risk_summary: riskSummaryForGrade(grade), attack_surface: attackSurface, top_risks: topRisks }; diff --git a/vscode-src/findingsTree.ts b/vscode-src/findingsTree.ts index 8bd4aba..277019c 100644 --- a/vscode-src/findingsTree.ts +++ b/vscode-src/findingsTree.ts @@ -20,6 +20,17 @@ function severityIcon(severity: Severity): vscode.ThemeIcon { } } +function locationBasename(location: string): string { + const separator = location.lastIndexOf(':'); + if (separator > 1) { + const suffix = location.slice(separator + 1); + if (/^\d+$/.test(suffix)) { + return path.basename(location.slice(0, separator)); + } + } + return path.basename(location); +} + class SeverityGroupNode extends vscode.TreeItem { constructor( public readonly severity: Severity, @@ -33,7 +44,7 @@ class SeverityGroupNode extends vscode.TreeItem { class FindingNode extends vscode.TreeItem { constructor(public readonly finding: Finding) { - const basename = finding.location ? path.basename(finding.location.split(':')[0]) : finding.rule_id; + const basename = finding.location ? locationBasename(finding.location) : finding.rule_id; super(`${finding.rule_id} ${basename}`, vscode.TreeItemCollapsibleState.None); this.description = finding.description; this.tooltip = new vscode.MarkdownString(