mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
test(ssh): scan remote payloads with the shared source walk
The ratchet had its own tree walk and comment stripper. The walk skipped neither node_modules/dist/.git nor dot-directories and excluded tests by `.test.ts` alone, so its importer count -- the guard's own goalpost -- could be wrong about what it scanned. The stripper was anchored to line start to dodge a `/*` inside a glob string, which silently skipped trailing comments; `stripComments` tracks quote state and handles both. Importer set re-derived against the shared walk: 15, floor unchanged at 10.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import { join, relative } from 'node:path'
|
||||
import { join } from 'node:path'
|
||||
import { scanSourceTree, stripComments } from '../../shared/source-scan/source-tree-scan'
|
||||
import { powerShellCommand } from './ssh-remote-powershell'
|
||||
|
||||
function decodePayload(command: string): string {
|
||||
@@ -43,29 +43,6 @@ describe('powerShellCommand', () => {
|
||||
|
||||
const MAIN_DIR = join(import.meta.dirname, '..')
|
||||
|
||||
function typeScriptSourcesUnder(dir: string, out: string[] = []): string[] {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
typeScriptSourcesUnder(path, out)
|
||||
} else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')) {
|
||||
out.push(path)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Whole-line `//` and block comments only, so string contents are never eaten.
|
||||
//
|
||||
// Both strips are anchored to the start of a line. An unanchored block strip would pair an
|
||||
// opening `/*` appearing inside a string (a glob such as 'src/*.ts') with any later comment
|
||||
// close — a JSDoc terminator, say — and delete everything between them, hiding a real
|
||||
// violation that sits in the gap. Verified: the unanchored form misses an injected
|
||||
// `Import-Module` sitting after a glob string.
|
||||
function withoutComments(source: string): string {
|
||||
return source.replace(/^[ \t]*\/\*[\s\S]*?\*\//gm, '').replace(/^[ \t]*\/\/.*$/gm, '')
|
||||
}
|
||||
|
||||
// Why: execution policy gates loading script FILES and nothing else, so dropping
|
||||
// `-ExecutionPolicy Bypass` is a no-op exactly while no remote payload loads one. That
|
||||
// invariant is what makes the switch safe to omit, and it was previously guarded by nothing:
|
||||
@@ -80,6 +57,11 @@ function withoutComments(source: string): string {
|
||||
// Limit: this is a source-text scan, so a script file reached only through a variable
|
||||
// (`& $scriptPath`) never appears in source and no pattern here can catch it — this narrows
|
||||
// the hole rather than sealing it. The invariant note on `powerShellCommand` covers the rest.
|
||||
//
|
||||
// Matched against `stripComments`, the shared quote-tracking stripper, so a construct named in
|
||||
// prose is not counted as code. A line-anchored regex pair cannot do this job: it either eats
|
||||
// live code by pairing a `/*` inside a glob string with a later comment close, or — anchoring
|
||||
// to avoid that — skips every trailing comment. Quote state is the only fix.
|
||||
const POLICY_GATED_CONSTRUCTS = [
|
||||
{
|
||||
label: 'a PowerShell script file (.ps1/.psm1)',
|
||||
@@ -130,13 +112,17 @@ const POLICY_GATED_CONSTRUCTS = [
|
||||
] as const
|
||||
|
||||
describe('remote PowerShell payload invariant', () => {
|
||||
const importers = typeScriptSourcesUnder(MAIN_DIR).filter((path) =>
|
||||
readFileSync(path, 'utf8').includes('ssh-remote-powershell')
|
||||
// `scanSourceTree` is the shared walk: it skips node_modules/dist/out/build/.git,
|
||||
// dot-directories and `__fixtures__`, and excludes tests by the shared `isTestFile` (which
|
||||
// also covers `.spec.ts`, `__tests__/` and `-test-harness.ts`). A hand-rolled walk that got
|
||||
// any of those wrong would move the floor below, which is this guard's own goalpost.
|
||||
const importers = scanSourceTree(MAIN_DIR).filter((file) =>
|
||||
file.source.includes('ssh-remote-powershell')
|
||||
)
|
||||
|
||||
it('finds the modules that build remote payloads', () => {
|
||||
// Guards the scan itself: a resolution change that emptied this list would make every
|
||||
// assertion below vacuously pass. 14 importers today.
|
||||
// assertion below vacuously pass. 15 importers today, re-derived against the shared walk.
|
||||
expect(importers.length).toBeGreaterThan(10)
|
||||
})
|
||||
|
||||
@@ -145,8 +131,8 @@ describe('remote PowerShell payload invariant', () => {
|
||||
pattern
|
||||
}) => {
|
||||
const offenders = importers
|
||||
.filter((path) => pattern.test(withoutComments(readFileSync(path, 'utf8'))))
|
||||
.map((path) => relative(MAIN_DIR, path))
|
||||
.filter((file) => pattern.test(stripComments(file.source)))
|
||||
.map((file) => file.relativePath)
|
||||
|
||||
expect(
|
||||
offenders,
|
||||
|
||||
Reference in New Issue
Block a user