gitx.do 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -353
- package/dist/do/logger.d.ts +50 -0
- package/dist/do/logger.d.ts.map +1 -0
- package/dist/do/logger.js +122 -0
- package/dist/do/logger.js.map +1 -0
- package/dist/{durable-object → do}/schema.d.ts +3 -3
- package/dist/do/schema.d.ts.map +1 -0
- package/dist/{durable-object → do}/schema.js +4 -3
- package/dist/do/schema.js.map +1 -0
- package/dist/do/types.d.ts +267 -0
- package/dist/do/types.d.ts.map +1 -0
- package/dist/do/types.js +62 -0
- package/dist/do/types.js.map +1 -0
- package/dist/index.d.ts +14 -469
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -483
- package/dist/index.js.map +1 -1
- package/package.json +13 -21
- package/dist/cli/commands/add.d.ts +0 -176
- package/dist/cli/commands/add.d.ts.map +0 -1
- package/dist/cli/commands/add.js +0 -979
- package/dist/cli/commands/add.js.map +0 -1
- package/dist/cli/commands/blame.d.ts +0 -259
- package/dist/cli/commands/blame.d.ts.map +0 -1
- package/dist/cli/commands/blame.js +0 -609
- package/dist/cli/commands/blame.js.map +0 -1
- package/dist/cli/commands/branch.d.ts +0 -249
- package/dist/cli/commands/branch.d.ts.map +0 -1
- package/dist/cli/commands/branch.js +0 -693
- package/dist/cli/commands/branch.js.map +0 -1
- package/dist/cli/commands/checkout.d.ts +0 -73
- package/dist/cli/commands/checkout.d.ts.map +0 -1
- package/dist/cli/commands/checkout.js +0 -725
- package/dist/cli/commands/checkout.js.map +0 -1
- package/dist/cli/commands/commit.d.ts +0 -182
- package/dist/cli/commands/commit.d.ts.map +0 -1
- package/dist/cli/commands/commit.js +0 -457
- package/dist/cli/commands/commit.js.map +0 -1
- package/dist/cli/commands/diff.d.ts +0 -464
- package/dist/cli/commands/diff.d.ts.map +0 -1
- package/dist/cli/commands/diff.js +0 -959
- package/dist/cli/commands/diff.js.map +0 -1
- package/dist/cli/commands/log.d.ts +0 -239
- package/dist/cli/commands/log.d.ts.map +0 -1
- package/dist/cli/commands/log.js +0 -535
- package/dist/cli/commands/log.js.map +0 -1
- package/dist/cli/commands/merge.d.ts +0 -106
- package/dist/cli/commands/merge.d.ts.map +0 -1
- package/dist/cli/commands/merge.js +0 -852
- package/dist/cli/commands/merge.js.map +0 -1
- package/dist/cli/commands/review.d.ts +0 -457
- package/dist/cli/commands/review.d.ts.map +0 -1
- package/dist/cli/commands/review.js +0 -558
- package/dist/cli/commands/review.js.map +0 -1
- package/dist/cli/commands/stash.d.ts +0 -157
- package/dist/cli/commands/stash.d.ts.map +0 -1
- package/dist/cli/commands/stash.js +0 -655
- package/dist/cli/commands/stash.js.map +0 -1
- package/dist/cli/commands/status.d.ts +0 -269
- package/dist/cli/commands/status.d.ts.map +0 -1
- package/dist/cli/commands/status.js +0 -492
- package/dist/cli/commands/status.js.map +0 -1
- package/dist/cli/commands/web.d.ts +0 -199
- package/dist/cli/commands/web.d.ts.map +0 -1
- package/dist/cli/commands/web.js +0 -697
- package/dist/cli/commands/web.js.map +0 -1
- package/dist/cli/fs-adapter.d.ts +0 -656
- package/dist/cli/fs-adapter.d.ts.map +0 -1
- package/dist/cli/fs-adapter.js +0 -1177
- package/dist/cli/fs-adapter.js.map +0 -1
- package/dist/cli/fsx-cli-adapter.d.ts +0 -359
- package/dist/cli/fsx-cli-adapter.d.ts.map +0 -1
- package/dist/cli/fsx-cli-adapter.js +0 -619
- package/dist/cli/fsx-cli-adapter.js.map +0 -1
- package/dist/cli/index.d.ts +0 -387
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -579
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/ui/components/DiffView.d.ts +0 -12
- package/dist/cli/ui/components/DiffView.d.ts.map +0 -1
- package/dist/cli/ui/components/DiffView.js +0 -11
- package/dist/cli/ui/components/DiffView.js.map +0 -1
- package/dist/cli/ui/components/ErrorDisplay.d.ts +0 -10
- package/dist/cli/ui/components/ErrorDisplay.d.ts.map +0 -1
- package/dist/cli/ui/components/ErrorDisplay.js +0 -11
- package/dist/cli/ui/components/ErrorDisplay.js.map +0 -1
- package/dist/cli/ui/components/FuzzySearch.d.ts +0 -15
- package/dist/cli/ui/components/FuzzySearch.d.ts.map +0 -1
- package/dist/cli/ui/components/FuzzySearch.js +0 -12
- package/dist/cli/ui/components/FuzzySearch.js.map +0 -1
- package/dist/cli/ui/components/LoadingSpinner.d.ts +0 -10
- package/dist/cli/ui/components/LoadingSpinner.d.ts.map +0 -1
- package/dist/cli/ui/components/LoadingSpinner.js +0 -10
- package/dist/cli/ui/components/LoadingSpinner.js.map +0 -1
- package/dist/cli/ui/components/NavigationList.d.ts +0 -14
- package/dist/cli/ui/components/NavigationList.d.ts.map +0 -1
- package/dist/cli/ui/components/NavigationList.js +0 -11
- package/dist/cli/ui/components/NavigationList.js.map +0 -1
- package/dist/cli/ui/components/ScrollableContent.d.ts +0 -13
- package/dist/cli/ui/components/ScrollableContent.d.ts.map +0 -1
- package/dist/cli/ui/components/ScrollableContent.js +0 -11
- package/dist/cli/ui/components/ScrollableContent.js.map +0 -1
- package/dist/cli/ui/components/index.d.ts +0 -7
- package/dist/cli/ui/components/index.d.ts.map +0 -1
- package/dist/cli/ui/components/index.js +0 -9
- package/dist/cli/ui/components/index.js.map +0 -1
- package/dist/cli/ui/terminal-ui.d.ts +0 -85
- package/dist/cli/ui/terminal-ui.d.ts.map +0 -1
- package/dist/cli/ui/terminal-ui.js +0 -121
- package/dist/cli/ui/terminal-ui.js.map +0 -1
- package/dist/do/BashModule.d.ts +0 -871
- package/dist/do/BashModule.d.ts.map +0 -1
- package/dist/do/BashModule.js +0 -1143
- package/dist/do/BashModule.js.map +0 -1
- package/dist/do/FsModule.d.ts +0 -612
- package/dist/do/FsModule.d.ts.map +0 -1
- package/dist/do/FsModule.js +0 -1120
- package/dist/do/FsModule.js.map +0 -1
- package/dist/do/GitModule.d.ts +0 -635
- package/dist/do/GitModule.d.ts.map +0 -1
- package/dist/do/GitModule.js +0 -784
- package/dist/do/GitModule.js.map +0 -1
- package/dist/do/GitRepoDO.d.ts +0 -281
- package/dist/do/GitRepoDO.d.ts.map +0 -1
- package/dist/do/GitRepoDO.js +0 -479
- package/dist/do/GitRepoDO.js.map +0 -1
- package/dist/do/bash-ast.d.ts +0 -246
- package/dist/do/bash-ast.d.ts.map +0 -1
- package/dist/do/bash-ast.js +0 -888
- package/dist/do/bash-ast.js.map +0 -1
- package/dist/do/container-executor.d.ts +0 -491
- package/dist/do/container-executor.d.ts.map +0 -1
- package/dist/do/container-executor.js +0 -731
- package/dist/do/container-executor.js.map +0 -1
- package/dist/do/index.d.ts +0 -53
- package/dist/do/index.d.ts.map +0 -1
- package/dist/do/index.js +0 -91
- package/dist/do/index.js.map +0 -1
- package/dist/do/tiered-storage.d.ts +0 -403
- package/dist/do/tiered-storage.d.ts.map +0 -1
- package/dist/do/tiered-storage.js +0 -689
- package/dist/do/tiered-storage.js.map +0 -1
- package/dist/do/withBash.d.ts +0 -231
- package/dist/do/withBash.d.ts.map +0 -1
- package/dist/do/withBash.js +0 -244
- package/dist/do/withBash.js.map +0 -1
- package/dist/do/withFs.d.ts +0 -237
- package/dist/do/withFs.d.ts.map +0 -1
- package/dist/do/withFs.js +0 -387
- package/dist/do/withFs.js.map +0 -1
- package/dist/do/withGit.d.ts +0 -180
- package/dist/do/withGit.d.ts.map +0 -1
- package/dist/do/withGit.js +0 -271
- package/dist/do/withGit.js.map +0 -1
- package/dist/durable-object/object-store.d.ts +0 -633
- package/dist/durable-object/object-store.d.ts.map +0 -1
- package/dist/durable-object/object-store.js +0 -1164
- package/dist/durable-object/object-store.js.map +0 -1
- package/dist/durable-object/schema.d.ts.map +0 -1
- package/dist/durable-object/schema.js.map +0 -1
- package/dist/durable-object/wal.d.ts +0 -416
- package/dist/durable-object/wal.d.ts.map +0 -1
- package/dist/durable-object/wal.js +0 -445
- package/dist/durable-object/wal.js.map +0 -1
- package/dist/mcp/adapter.d.ts +0 -772
- package/dist/mcp/adapter.d.ts.map +0 -1
- package/dist/mcp/adapter.js +0 -895
- package/dist/mcp/adapter.js.map +0 -1
- package/dist/mcp/sandbox/miniflare-evaluator.d.ts +0 -22
- package/dist/mcp/sandbox/miniflare-evaluator.d.ts.map +0 -1
- package/dist/mcp/sandbox/miniflare-evaluator.js +0 -140
- package/dist/mcp/sandbox/miniflare-evaluator.js.map +0 -1
- package/dist/mcp/sandbox/object-store-proxy.d.ts +0 -32
- package/dist/mcp/sandbox/object-store-proxy.d.ts.map +0 -1
- package/dist/mcp/sandbox/object-store-proxy.js +0 -30
- package/dist/mcp/sandbox/object-store-proxy.js.map +0 -1
- package/dist/mcp/sandbox/template.d.ts +0 -17
- package/dist/mcp/sandbox/template.d.ts.map +0 -1
- package/dist/mcp/sandbox/template.js +0 -71
- package/dist/mcp/sandbox/template.js.map +0 -1
- package/dist/mcp/sandbox.d.ts +0 -764
- package/dist/mcp/sandbox.d.ts.map +0 -1
- package/dist/mcp/sandbox.js +0 -1362
- package/dist/mcp/sandbox.js.map +0 -1
- package/dist/mcp/sdk-adapter.d.ts +0 -835
- package/dist/mcp/sdk-adapter.d.ts.map +0 -1
- package/dist/mcp/sdk-adapter.js +0 -974
- package/dist/mcp/sdk-adapter.js.map +0 -1
- package/dist/mcp/tools/do.d.ts +0 -32
- package/dist/mcp/tools/do.d.ts.map +0 -1
- package/dist/mcp/tools/do.js +0 -117
- package/dist/mcp/tools/do.js.map +0 -1
- package/dist/mcp/tools.d.ts +0 -548
- package/dist/mcp/tools.d.ts.map +0 -1
- package/dist/mcp/tools.js +0 -3170
- package/dist/mcp/tools.js.map +0 -1
- package/dist/ops/blame.d.ts +0 -551
- package/dist/ops/blame.d.ts.map +0 -1
- package/dist/ops/blame.js +0 -1037
- package/dist/ops/blame.js.map +0 -1
- package/dist/ops/branch.d.ts +0 -766
- package/dist/ops/branch.d.ts.map +0 -1
- package/dist/ops/branch.js +0 -950
- package/dist/ops/branch.js.map +0 -1
- package/dist/ops/commit-traversal.d.ts +0 -349
- package/dist/ops/commit-traversal.d.ts.map +0 -1
- package/dist/ops/commit-traversal.js +0 -821
- package/dist/ops/commit-traversal.js.map +0 -1
- package/dist/ops/commit.d.ts +0 -555
- package/dist/ops/commit.d.ts.map +0 -1
- package/dist/ops/commit.js +0 -826
- package/dist/ops/commit.js.map +0 -1
- package/dist/ops/merge-base.d.ts +0 -397
- package/dist/ops/merge-base.d.ts.map +0 -1
- package/dist/ops/merge-base.js +0 -691
- package/dist/ops/merge-base.js.map +0 -1
- package/dist/ops/merge.d.ts +0 -855
- package/dist/ops/merge.d.ts.map +0 -1
- package/dist/ops/merge.js +0 -1551
- package/dist/ops/merge.js.map +0 -1
- package/dist/ops/tag.d.ts +0 -247
- package/dist/ops/tag.d.ts.map +0 -1
- package/dist/ops/tag.js +0 -649
- package/dist/ops/tag.js.map +0 -1
- package/dist/ops/tree-builder.d.ts +0 -178
- package/dist/ops/tree-builder.d.ts.map +0 -1
- package/dist/ops/tree-builder.js +0 -271
- package/dist/ops/tree-builder.js.map +0 -1
- package/dist/ops/tree-diff.d.ts +0 -291
- package/dist/ops/tree-diff.d.ts.map +0 -1
- package/dist/ops/tree-diff.js +0 -705
- package/dist/ops/tree-diff.js.map +0 -1
- package/dist/pack/delta.d.ts +0 -248
- package/dist/pack/delta.d.ts.map +0 -1
- package/dist/pack/delta.js +0 -740
- package/dist/pack/delta.js.map +0 -1
- package/dist/pack/format.d.ts +0 -446
- package/dist/pack/format.d.ts.map +0 -1
- package/dist/pack/format.js +0 -572
- package/dist/pack/format.js.map +0 -1
- package/dist/pack/full-generation.d.ts +0 -612
- package/dist/pack/full-generation.d.ts.map +0 -1
- package/dist/pack/full-generation.js +0 -1378
- package/dist/pack/full-generation.js.map +0 -1
- package/dist/pack/generation.d.ts +0 -441
- package/dist/pack/generation.d.ts.map +0 -1
- package/dist/pack/generation.js +0 -707
- package/dist/pack/generation.js.map +0 -1
- package/dist/pack/index.d.ts +0 -502
- package/dist/pack/index.d.ts.map +0 -1
- package/dist/pack/index.js +0 -833
- package/dist/pack/index.js.map +0 -1
- package/dist/refs/branch.d.ts +0 -683
- package/dist/refs/branch.d.ts.map +0 -1
- package/dist/refs/branch.js +0 -881
- package/dist/refs/branch.js.map +0 -1
- package/dist/refs/storage.d.ts +0 -833
- package/dist/refs/storage.d.ts.map +0 -1
- package/dist/refs/storage.js +0 -1023
- package/dist/refs/storage.js.map +0 -1
- package/dist/refs/tag.d.ts +0 -860
- package/dist/refs/tag.d.ts.map +0 -1
- package/dist/refs/tag.js +0 -996
- package/dist/refs/tag.js.map +0 -1
- package/dist/storage/backend.d.ts +0 -425
- package/dist/storage/backend.d.ts.map +0 -1
- package/dist/storage/backend.js +0 -41
- package/dist/storage/backend.js.map +0 -1
- package/dist/storage/fsx-adapter.d.ts +0 -204
- package/dist/storage/fsx-adapter.d.ts.map +0 -1
- package/dist/storage/fsx-adapter.js +0 -518
- package/dist/storage/fsx-adapter.js.map +0 -1
- package/dist/storage/lru-cache.d.ts +0 -691
- package/dist/storage/lru-cache.d.ts.map +0 -1
- package/dist/storage/lru-cache.js +0 -813
- package/dist/storage/lru-cache.js.map +0 -1
- package/dist/storage/object-index.d.ts +0 -585
- package/dist/storage/object-index.d.ts.map +0 -1
- package/dist/storage/object-index.js +0 -532
- package/dist/storage/object-index.js.map +0 -1
- package/dist/storage/r2-pack.d.ts +0 -1257
- package/dist/storage/r2-pack.d.ts.map +0 -1
- package/dist/storage/r2-pack.js +0 -1773
- package/dist/storage/r2-pack.js.map +0 -1
- package/dist/tiered/cdc-pipeline.d.ts +0 -1888
- package/dist/tiered/cdc-pipeline.d.ts.map +0 -1
- package/dist/tiered/cdc-pipeline.js +0 -1880
- package/dist/tiered/cdc-pipeline.js.map +0 -1
- package/dist/tiered/migration.d.ts +0 -1104
- package/dist/tiered/migration.d.ts.map +0 -1
- package/dist/tiered/migration.js +0 -1217
- package/dist/tiered/migration.js.map +0 -1
- package/dist/tiered/parquet-writer.d.ts +0 -1145
- package/dist/tiered/parquet-writer.d.ts.map +0 -1
- package/dist/tiered/parquet-writer.js +0 -1183
- package/dist/tiered/parquet-writer.js.map +0 -1
- package/dist/tiered/read-path.d.ts +0 -835
- package/dist/tiered/read-path.d.ts.map +0 -1
- package/dist/tiered/read-path.js +0 -487
- package/dist/tiered/read-path.js.map +0 -1
- package/dist/types/capability.d.ts +0 -1385
- package/dist/types/capability.d.ts.map +0 -1
- package/dist/types/capability.js +0 -36
- package/dist/types/capability.js.map +0 -1
- package/dist/types/index.d.ts +0 -13
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -18
- package/dist/types/index.js.map +0 -1
- package/dist/types/interfaces.d.ts +0 -673
- package/dist/types/interfaces.d.ts.map +0 -1
- package/dist/types/interfaces.js +0 -26
- package/dist/types/interfaces.js.map +0 -1
- package/dist/types/objects.d.ts +0 -692
- package/dist/types/objects.d.ts.map +0 -1
- package/dist/types/objects.js +0 -837
- package/dist/types/objects.js.map +0 -1
- package/dist/types/storage.d.ts +0 -603
- package/dist/types/storage.d.ts.map +0 -1
- package/dist/types/storage.js +0 -191
- package/dist/types/storage.js.map +0 -1
- package/dist/types/worker-loader.d.ts +0 -60
- package/dist/types/worker-loader.d.ts.map +0 -1
- package/dist/types/worker-loader.js +0 -62
- package/dist/types/worker-loader.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -198
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js +0 -272
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/sha1.d.ts +0 -325
- package/dist/utils/sha1.d.ts.map +0 -1
- package/dist/utils/sha1.js +0 -635
- package/dist/utils/sha1.js.map +0 -1
- package/dist/wire/capabilities.d.ts +0 -1044
- package/dist/wire/capabilities.d.ts.map +0 -1
- package/dist/wire/capabilities.js +0 -941
- package/dist/wire/capabilities.js.map +0 -1
- package/dist/wire/path-security.d.ts +0 -157
- package/dist/wire/path-security.d.ts.map +0 -1
- package/dist/wire/path-security.js +0 -307
- package/dist/wire/path-security.js.map +0 -1
- package/dist/wire/pkt-line.d.ts +0 -345
- package/dist/wire/pkt-line.d.ts.map +0 -1
- package/dist/wire/pkt-line.js +0 -381
- package/dist/wire/pkt-line.js.map +0 -1
- package/dist/wire/receive-pack.d.ts +0 -1059
- package/dist/wire/receive-pack.d.ts.map +0 -1
- package/dist/wire/receive-pack.js +0 -1414
- package/dist/wire/receive-pack.js.map +0 -1
- package/dist/wire/smart-http.d.ts +0 -799
- package/dist/wire/smart-http.d.ts.map +0 -1
- package/dist/wire/smart-http.js +0 -945
- package/dist/wire/smart-http.js.map +0 -1
- package/dist/wire/upload-pack.d.ts +0 -727
- package/dist/wire/upload-pack.d.ts.map +0 -1
- package/dist/wire/upload-pack.js +0 -1141
- package/dist/wire/upload-pack.js.map +0 -1
package/dist/do/bash-ast.js
DELETED
|
@@ -1,888 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Bash AST Parser and Safety Analyzer
|
|
3
|
-
*
|
|
4
|
-
* This module provides a bash command parser that produces an Abstract Syntax Tree (AST)
|
|
5
|
-
* for shell commands, along with safety analysis based on AST node inspection.
|
|
6
|
-
*
|
|
7
|
-
* The parser handles:
|
|
8
|
-
* - Simple commands (ls, cat, etc.)
|
|
9
|
-
* - Pipelines (cmd1 | cmd2)
|
|
10
|
-
* - Command lists (cmd1 && cmd2, cmd1 || cmd2, cmd1 ; cmd2)
|
|
11
|
-
* - Subshells ($(...) and `...`)
|
|
12
|
-
* - Redirections (>, >>, <, 2>&1)
|
|
13
|
-
* - Variable expansion ($VAR, ${VAR})
|
|
14
|
-
* - Quoting (single, double, and escape)
|
|
15
|
-
*
|
|
16
|
-
* @module do/bash-ast
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```typescript
|
|
20
|
-
* import { parseBashCommand, analyzeASTSafety } from './bash-ast'
|
|
21
|
-
*
|
|
22
|
-
* const ast = parseBashCommand('rm -rf /')
|
|
23
|
-
* const safety = analyzeASTSafety(ast)
|
|
24
|
-
* if (safety.dangerous) {
|
|
25
|
-
* console.log(`Command blocked: ${safety.reason}`)
|
|
26
|
-
* }
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
/**
|
|
30
|
-
* Tokenize a bash command string.
|
|
31
|
-
*/
|
|
32
|
-
function tokenize(input) {
|
|
33
|
-
const tokens = [];
|
|
34
|
-
let pos = 0;
|
|
35
|
-
const operators = ['&&', '||', ';;', ';&', ';;&', '|&', '|', ';', '&', '(', ')'];
|
|
36
|
-
const redirectOps = ['>>>', '>>', '>&', '&>>', '&>', '<<-', '<<<', '<<', '<>', '<&', '<', '>|', '>'];
|
|
37
|
-
while (pos < input.length) {
|
|
38
|
-
// Skip whitespace
|
|
39
|
-
if (/\s/.test(input[pos]) && input[pos] !== '\n') {
|
|
40
|
-
pos++;
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
// Newline
|
|
44
|
-
if (input[pos] === '\n') {
|
|
45
|
-
tokens.push({ type: 'newline', value: '\n', start: pos, end: pos + 1 });
|
|
46
|
-
pos++;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
// Comments
|
|
50
|
-
if (input[pos] === '#') {
|
|
51
|
-
while (pos < input.length && input[pos] !== '\n') {
|
|
52
|
-
pos++;
|
|
53
|
-
}
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
// Check for redirect operators (before general operators)
|
|
57
|
-
let foundRedirect = false;
|
|
58
|
-
for (const op of redirectOps) {
|
|
59
|
-
if (input.slice(pos, pos + op.length) === op) {
|
|
60
|
-
// Check for number prefix (like 2>)
|
|
61
|
-
let actualStart = pos;
|
|
62
|
-
if (pos > 0 && /\d/.test(input[pos - 1])) {
|
|
63
|
-
// Check if previous char was actually a number redirect prefix
|
|
64
|
-
const prevToken = tokens[tokens.length - 1];
|
|
65
|
-
if (prevToken && prevToken.type === 'word' && /^\d+$/.test(prevToken.value)) {
|
|
66
|
-
tokens.pop();
|
|
67
|
-
actualStart = prevToken.start;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
tokens.push({ type: 'redirect', value: input.slice(actualStart, pos + op.length), start: actualStart, end: pos + op.length });
|
|
71
|
-
pos += op.length;
|
|
72
|
-
foundRedirect = true;
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (foundRedirect)
|
|
77
|
-
continue;
|
|
78
|
-
// Check for operators
|
|
79
|
-
let foundOp = false;
|
|
80
|
-
for (const op of operators) {
|
|
81
|
-
if (input.slice(pos, pos + op.length) === op) {
|
|
82
|
-
tokens.push({ type: 'operator', value: op, start: pos, end: pos + op.length });
|
|
83
|
-
pos += op.length;
|
|
84
|
-
foundOp = true;
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (foundOp)
|
|
89
|
-
continue;
|
|
90
|
-
// Word (including quoted strings)
|
|
91
|
-
const wordStart = pos;
|
|
92
|
-
let word = '';
|
|
93
|
-
while (pos < input.length) {
|
|
94
|
-
const ch = input[pos];
|
|
95
|
-
// End of word
|
|
96
|
-
if (/\s/.test(ch) || operators.some(op => input.slice(pos, pos + op.length) === op) ||
|
|
97
|
-
redirectOps.some(op => input.slice(pos, pos + op.length) === op)) {
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
// Single quoted string
|
|
101
|
-
if (ch === "'") {
|
|
102
|
-
pos++;
|
|
103
|
-
while (pos < input.length && input[pos] !== "'") {
|
|
104
|
-
word += input[pos];
|
|
105
|
-
pos++;
|
|
106
|
-
}
|
|
107
|
-
if (pos < input.length)
|
|
108
|
-
pos++; // Skip closing quote
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
// Double quoted string
|
|
112
|
-
if (ch === '"') {
|
|
113
|
-
pos++;
|
|
114
|
-
while (pos < input.length && input[pos] !== '"') {
|
|
115
|
-
if (input[pos] === '\\' && pos + 1 < input.length) {
|
|
116
|
-
word += input[pos + 1];
|
|
117
|
-
pos += 2;
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
word += input[pos];
|
|
121
|
-
pos++;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (pos < input.length)
|
|
125
|
-
pos++; // Skip closing quote
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
// Backslash escape
|
|
129
|
-
if (ch === '\\' && pos + 1 < input.length) {
|
|
130
|
-
word += input[pos + 1];
|
|
131
|
-
pos += 2;
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
// Command substitution $()
|
|
135
|
-
if (ch === '$' && pos + 1 < input.length && input[pos + 1] === '(') {
|
|
136
|
-
let depth = 1;
|
|
137
|
-
word += '$(';
|
|
138
|
-
pos += 2;
|
|
139
|
-
while (pos < input.length && depth > 0) {
|
|
140
|
-
if (input[pos] === '(')
|
|
141
|
-
depth++;
|
|
142
|
-
else if (input[pos] === ')')
|
|
143
|
-
depth--;
|
|
144
|
-
if (depth > 0)
|
|
145
|
-
word += input[pos];
|
|
146
|
-
pos++;
|
|
147
|
-
}
|
|
148
|
-
word += ')';
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
// Backtick substitution
|
|
152
|
-
if (ch === '`') {
|
|
153
|
-
word += ch;
|
|
154
|
-
pos++;
|
|
155
|
-
while (pos < input.length && input[pos] !== '`') {
|
|
156
|
-
if (input[pos] === '\\' && pos + 1 < input.length) {
|
|
157
|
-
word += input[pos] + input[pos + 1];
|
|
158
|
-
pos += 2;
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
word += input[pos];
|
|
162
|
-
pos++;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
if (pos < input.length) {
|
|
166
|
-
word += '`';
|
|
167
|
-
pos++;
|
|
168
|
-
}
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
// Variable expansion
|
|
172
|
-
if (ch === '$') {
|
|
173
|
-
word += ch;
|
|
174
|
-
pos++;
|
|
175
|
-
if (pos < input.length && input[pos] === '{') {
|
|
176
|
-
word += '{';
|
|
177
|
-
pos++;
|
|
178
|
-
while (pos < input.length && input[pos] !== '}') {
|
|
179
|
-
word += input[pos];
|
|
180
|
-
pos++;
|
|
181
|
-
}
|
|
182
|
-
if (pos < input.length) {
|
|
183
|
-
word += '}';
|
|
184
|
-
pos++;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
while (pos < input.length && /[a-zA-Z0-9_]/.test(input[pos])) {
|
|
189
|
-
word += input[pos];
|
|
190
|
-
pos++;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
// Regular character
|
|
196
|
-
word += ch;
|
|
197
|
-
pos++;
|
|
198
|
-
}
|
|
199
|
-
if (word.length > 0) {
|
|
200
|
-
tokens.push({ type: 'word', value: word, start: wordStart, end: pos });
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
tokens.push({ type: 'eof', value: '', start: pos, end: pos });
|
|
204
|
-
return tokens;
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Peek at the current token.
|
|
208
|
-
*/
|
|
209
|
-
function peek(state) {
|
|
210
|
-
return state.tokens[state.pos] ?? { type: 'eof', value: '', start: state.input.length, end: state.input.length };
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Consume the current token.
|
|
214
|
-
*/
|
|
215
|
-
function consume(state) {
|
|
216
|
-
return state.tokens[state.pos++] ?? { type: 'eof', value: '', start: state.input.length, end: state.input.length };
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Check if current token matches.
|
|
220
|
-
*/
|
|
221
|
-
function match(state, type, value) {
|
|
222
|
-
const token = peek(state);
|
|
223
|
-
if (token.type !== type)
|
|
224
|
-
return false;
|
|
225
|
-
if (value !== undefined && token.value !== value)
|
|
226
|
-
return false;
|
|
227
|
-
return true;
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Parse a word token into a WordNode.
|
|
231
|
-
*/
|
|
232
|
-
function parseWord(state) {
|
|
233
|
-
const token = consume(state);
|
|
234
|
-
let quoted = 'none';
|
|
235
|
-
let expandable = false;
|
|
236
|
-
// Check for quoting and expansion
|
|
237
|
-
if (token.value.includes('$'))
|
|
238
|
-
expandable = true;
|
|
239
|
-
if (token.value.startsWith("'"))
|
|
240
|
-
quoted = 'single';
|
|
241
|
-
else if (token.value.startsWith('"'))
|
|
242
|
-
quoted = 'double';
|
|
243
|
-
else if (token.value.includes('\\'))
|
|
244
|
-
quoted = 'escaped';
|
|
245
|
-
return {
|
|
246
|
-
type: 'word',
|
|
247
|
-
raw: state.input.slice(token.start, token.end),
|
|
248
|
-
value: token.value,
|
|
249
|
-
quoted,
|
|
250
|
-
expandable,
|
|
251
|
-
start: token.start,
|
|
252
|
-
end: token.end,
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* Parse a redirection.
|
|
257
|
-
*/
|
|
258
|
-
function parseRedirect(state) {
|
|
259
|
-
if (!match(state, 'redirect'))
|
|
260
|
-
return null;
|
|
261
|
-
const redirectToken = consume(state);
|
|
262
|
-
const operator = redirectToken.value.replace(/^\d+/, '');
|
|
263
|
-
const fd = /^\d+/.test(redirectToken.value) ? parseInt(redirectToken.value.match(/^\d+/)[0]) : undefined;
|
|
264
|
-
// Get target
|
|
265
|
-
if (!match(state, 'word')) {
|
|
266
|
-
// Missing redirect target
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
const target = parseWord(state);
|
|
270
|
-
return {
|
|
271
|
-
type: 'redirect',
|
|
272
|
-
raw: state.input.slice(redirectToken.start, target.end),
|
|
273
|
-
operator,
|
|
274
|
-
target,
|
|
275
|
-
fd,
|
|
276
|
-
start: redirectToken.start,
|
|
277
|
-
end: target.end,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Parse a simple command.
|
|
282
|
-
*/
|
|
283
|
-
function parseCommand(state) {
|
|
284
|
-
// Check for subshell
|
|
285
|
-
if (match(state, 'operator', '(')) {
|
|
286
|
-
const startToken = consume(state);
|
|
287
|
-
const body = parseList(state);
|
|
288
|
-
if (match(state, 'operator', ')')) {
|
|
289
|
-
consume(state);
|
|
290
|
-
}
|
|
291
|
-
return {
|
|
292
|
-
type: 'subshell',
|
|
293
|
-
raw: state.input.slice(startToken.start, peek(state).start),
|
|
294
|
-
body,
|
|
295
|
-
style: '()',
|
|
296
|
-
start: startToken.start,
|
|
297
|
-
end: peek(state).start,
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
// Skip newlines
|
|
301
|
-
while (match(state, 'newline'))
|
|
302
|
-
consume(state);
|
|
303
|
-
if (!match(state, 'word'))
|
|
304
|
-
return null;
|
|
305
|
-
const startPos = peek(state).start;
|
|
306
|
-
const assignments = [];
|
|
307
|
-
const args = [];
|
|
308
|
-
const redirects = [];
|
|
309
|
-
// Check for assignments at the start
|
|
310
|
-
while (match(state, 'word') && /^[a-zA-Z_][a-zA-Z0-9_]*=/.test(peek(state).value)) {
|
|
311
|
-
const token = consume(state);
|
|
312
|
-
const eqPos = token.value.indexOf('=');
|
|
313
|
-
assignments.push({
|
|
314
|
-
type: 'assignment',
|
|
315
|
-
raw: token.value,
|
|
316
|
-
name: token.value.slice(0, eqPos),
|
|
317
|
-
value: {
|
|
318
|
-
type: 'word',
|
|
319
|
-
raw: token.value.slice(eqPos + 1),
|
|
320
|
-
value: token.value.slice(eqPos + 1),
|
|
321
|
-
quoted: 'none',
|
|
322
|
-
expandable: token.value.slice(eqPos + 1).includes('$'),
|
|
323
|
-
start: token.start + eqPos + 1,
|
|
324
|
-
end: token.end,
|
|
325
|
-
},
|
|
326
|
-
start: token.start,
|
|
327
|
-
end: token.end,
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
// Get command name
|
|
331
|
-
if (!match(state, 'word')) {
|
|
332
|
-
// Assignment-only command
|
|
333
|
-
if (assignments.length > 0) {
|
|
334
|
-
const lastAssign = assignments[assignments.length - 1];
|
|
335
|
-
return {
|
|
336
|
-
type: 'command',
|
|
337
|
-
raw: state.input.slice(startPos, lastAssign.end),
|
|
338
|
-
name: { type: 'word', raw: '', value: '', quoted: 'none', expandable: false, start: startPos, end: startPos },
|
|
339
|
-
args: [],
|
|
340
|
-
redirects: [],
|
|
341
|
-
assignments,
|
|
342
|
-
background: false,
|
|
343
|
-
start: startPos,
|
|
344
|
-
end: lastAssign.end,
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
return null;
|
|
348
|
-
}
|
|
349
|
-
const name = parseWord(state);
|
|
350
|
-
// Parse arguments and redirects
|
|
351
|
-
while (true) {
|
|
352
|
-
const redirect = parseRedirect(state);
|
|
353
|
-
if (redirect) {
|
|
354
|
-
redirects.push(redirect);
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
if (match(state, 'word')) {
|
|
358
|
-
args.push(parseWord(state));
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
break;
|
|
362
|
-
}
|
|
363
|
-
const endPos = args.length > 0 ? args[args.length - 1].end :
|
|
364
|
-
redirects.length > 0 ? redirects[redirects.length - 1].end :
|
|
365
|
-
name.end;
|
|
366
|
-
return {
|
|
367
|
-
type: 'command',
|
|
368
|
-
raw: state.input.slice(startPos, endPos),
|
|
369
|
-
name,
|
|
370
|
-
args,
|
|
371
|
-
redirects,
|
|
372
|
-
assignments,
|
|
373
|
-
background: false,
|
|
374
|
-
start: startPos,
|
|
375
|
-
end: endPos,
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* Parse a pipeline.
|
|
380
|
-
*/
|
|
381
|
-
function parsePipeline(state) {
|
|
382
|
-
// Check for negation
|
|
383
|
-
const negated = match(state, 'word') && peek(state).value === '!';
|
|
384
|
-
if (negated)
|
|
385
|
-
consume(state);
|
|
386
|
-
const commands = [];
|
|
387
|
-
const first = parseCommand(state);
|
|
388
|
-
if (!first)
|
|
389
|
-
return null;
|
|
390
|
-
commands.push(first);
|
|
391
|
-
// Parse pipe chain
|
|
392
|
-
while (match(state, 'operator', '|') || match(state, 'operator', '|&')) {
|
|
393
|
-
consume(state);
|
|
394
|
-
while (match(state, 'newline'))
|
|
395
|
-
consume(state);
|
|
396
|
-
const next = parseCommand(state);
|
|
397
|
-
if (!next)
|
|
398
|
-
break;
|
|
399
|
-
commands.push(next);
|
|
400
|
-
}
|
|
401
|
-
const startPos = commands[0].start;
|
|
402
|
-
const endPos = commands[commands.length - 1].end;
|
|
403
|
-
return {
|
|
404
|
-
type: 'pipeline',
|
|
405
|
-
raw: state.input.slice(startPos, endPos),
|
|
406
|
-
commands,
|
|
407
|
-
negated,
|
|
408
|
-
start: startPos,
|
|
409
|
-
end: endPos,
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Parse a command list.
|
|
414
|
-
*/
|
|
415
|
-
function parseList(state) {
|
|
416
|
-
const pipelines = [];
|
|
417
|
-
const operators = [];
|
|
418
|
-
// Skip initial newlines
|
|
419
|
-
while (match(state, 'newline'))
|
|
420
|
-
consume(state);
|
|
421
|
-
const first = parsePipeline(state);
|
|
422
|
-
if (first) {
|
|
423
|
-
pipelines.push(first);
|
|
424
|
-
while (true) {
|
|
425
|
-
// Check for list operators
|
|
426
|
-
if (match(state, 'operator', '&&') || match(state, 'operator', '||') ||
|
|
427
|
-
match(state, 'operator', ';') || match(state, 'operator', '&')) {
|
|
428
|
-
const op = consume(state).value;
|
|
429
|
-
operators.push(op);
|
|
430
|
-
// Mark previous command as background if &
|
|
431
|
-
if (op === '&' && pipelines.length > 0) {
|
|
432
|
-
const lastPipeline = pipelines[pipelines.length - 1];
|
|
433
|
-
if (lastPipeline.commands.length > 0) {
|
|
434
|
-
const lastCmd = lastPipeline.commands[lastPipeline.commands.length - 1];
|
|
435
|
-
if (lastCmd.type === 'command') {
|
|
436
|
-
lastCmd.background = true;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
// Skip newlines after operator
|
|
441
|
-
while (match(state, 'newline'))
|
|
442
|
-
consume(state);
|
|
443
|
-
const next = parsePipeline(state);
|
|
444
|
-
if (next) {
|
|
445
|
-
pipelines.push(next);
|
|
446
|
-
}
|
|
447
|
-
else {
|
|
448
|
-
break;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
else if (match(state, 'newline')) {
|
|
452
|
-
consume(state);
|
|
453
|
-
while (match(state, 'newline'))
|
|
454
|
-
consume(state);
|
|
455
|
-
// Check if there's more
|
|
456
|
-
const next = parsePipeline(state);
|
|
457
|
-
if (next) {
|
|
458
|
-
operators.push(';');
|
|
459
|
-
pipelines.push(next);
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
break;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
else {
|
|
466
|
-
break;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
const startPos = pipelines.length > 0 ? pipelines[0].start : 0;
|
|
471
|
-
const endPos = pipelines.length > 0 ? pipelines[pipelines.length - 1].end : 0;
|
|
472
|
-
return {
|
|
473
|
-
type: 'list',
|
|
474
|
-
raw: state.input.slice(startPos, endPos),
|
|
475
|
-
pipelines,
|
|
476
|
-
operators,
|
|
477
|
-
start: startPos,
|
|
478
|
-
end: endPos,
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Parse a bash command string into an AST.
|
|
483
|
-
*
|
|
484
|
-
* @param input - The bash command string to parse
|
|
485
|
-
* @returns The parsed AST (ListNode at the top level)
|
|
486
|
-
*
|
|
487
|
-
* @example
|
|
488
|
-
* ```typescript
|
|
489
|
-
* const ast = parseBashCommand('ls -la | grep foo && echo done')
|
|
490
|
-
* // Returns a ListNode with two pipelines
|
|
491
|
-
* ```
|
|
492
|
-
*/
|
|
493
|
-
export function parseBashCommand(input) {
|
|
494
|
-
const tokens = tokenize(input);
|
|
495
|
-
const state = { tokens, pos: 0, input };
|
|
496
|
-
return parseList(state);
|
|
497
|
-
}
|
|
498
|
-
// ============================================================================
|
|
499
|
-
// Safety Analyzer
|
|
500
|
-
// ============================================================================
|
|
501
|
-
/**
|
|
502
|
-
* Commands considered dangerous by default.
|
|
503
|
-
*/
|
|
504
|
-
const DANGEROUS_COMMANDS = new Set([
|
|
505
|
-
'rm', 'rmdir', 'dd', 'mkfs', 'fdisk', 'format',
|
|
506
|
-
'shutdown', 'reboot', 'halt', 'poweroff', 'init',
|
|
507
|
-
'kill', 'killall', 'pkill',
|
|
508
|
-
'chmod', 'chown', 'chgrp',
|
|
509
|
-
'mount', 'umount', 'mkswap', 'swapon', 'swapoff',
|
|
510
|
-
]);
|
|
511
|
-
/**
|
|
512
|
-
* Commands that are considered safe (read-only).
|
|
513
|
-
*/
|
|
514
|
-
const SAFE_COMMANDS = new Set([
|
|
515
|
-
'cat', 'ls', 'pwd', 'echo', 'head', 'tail', 'wc',
|
|
516
|
-
'grep', 'find', 'which', 'whereis', 'type', 'file',
|
|
517
|
-
'date', 'whoami', 'hostname', 'uname', 'env', 'printenv',
|
|
518
|
-
'basename', 'dirname', 'realpath', 'readlink',
|
|
519
|
-
]);
|
|
520
|
-
/**
|
|
521
|
-
* Commands with low impact (create files/dirs).
|
|
522
|
-
*/
|
|
523
|
-
const LOW_IMPACT_COMMANDS = new Set([
|
|
524
|
-
'touch', 'mkdir', 'cp', 'tee',
|
|
525
|
-
]);
|
|
526
|
-
/**
|
|
527
|
-
* Commands with medium impact (modify files).
|
|
528
|
-
*/
|
|
529
|
-
const MEDIUM_IMPACT_COMMANDS = new Set([
|
|
530
|
-
'mv', 'sed', 'awk', 'sort', 'uniq', 'cut', 'paste',
|
|
531
|
-
'tr', 'patch', 'diff',
|
|
532
|
-
]);
|
|
533
|
-
/**
|
|
534
|
-
* Critical patterns that should ALWAYS be blocked, regardless of confirmation.
|
|
535
|
-
* These patterns represent commands that could cause catastrophic, irreversible damage.
|
|
536
|
-
*/
|
|
537
|
-
const CRITICAL_PATTERNS = [
|
|
538
|
-
// rm -rf / or rm -rf /* (delete entire filesystem)
|
|
539
|
-
{ pattern: /\brm\s+(-[rfvI]+\s+)*\/\s*$/, message: 'Cannot execute rm targeting root filesystem' },
|
|
540
|
-
{ pattern: /\brm\s+(-[rfvI]+\s+)*\/\*/, message: 'Cannot execute rm with wildcard on root' },
|
|
541
|
-
// rm -rf with --no-preserve-root (explicit bypass of safety)
|
|
542
|
-
{ pattern: /\brm\s+.*--no-preserve-root/, message: 'Cannot execute rm with --no-preserve-root' },
|
|
543
|
-
// Fork bomb patterns
|
|
544
|
-
{ pattern: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, message: 'Fork bomb detected' },
|
|
545
|
-
{ pattern: /\.\(\)\s*\{\s*\.\s*\|\s*\.\s*&\s*\}\s*;\s*\./, message: 'Fork bomb variant detected' },
|
|
546
|
-
// Writing random data to block devices
|
|
547
|
-
{ pattern: /\bdd\s+.*if=\/dev\/(u?random|zero)\s+.*of=\/dev\/[hs]d[a-z]/, message: 'Cannot write to disk device' },
|
|
548
|
-
{ pattern: /\bdd\s+.*of=\/dev\/[hs]d[a-z].*if=\/dev\/(u?random|zero)/, message: 'Cannot write to disk device' },
|
|
549
|
-
// Overwriting MBR
|
|
550
|
-
{ pattern: /\bdd\s+.*of=\/dev\/[hs]d[a-z]\s+.*bs=\d+\s+.*count=1/, message: 'Cannot overwrite disk boot sector' },
|
|
551
|
-
// mkfs on system devices without confirmation
|
|
552
|
-
{ pattern: /\bmkfs(\.\w+)?\s+(-[a-zA-Z]+\s+)*\/dev\/[hs]d[a-z]\d*/, message: 'Cannot format disk device' },
|
|
553
|
-
// Direct writes to /dev/sda, /dev/hda, /dev/nvme, etc.
|
|
554
|
-
{ pattern: />\s*\/dev\/[hs]d[a-z]/, message: 'Cannot redirect output to disk device' },
|
|
555
|
-
{ pattern: />\s*\/dev\/nvme\d+n\d+/, message: 'Cannot redirect output to NVMe device' },
|
|
556
|
-
// Kernel panic triggers
|
|
557
|
-
{ pattern: /echo\s+[cso]\s*>\s*\/proc\/sysrq-trigger/, message: 'Cannot trigger kernel sysrq' },
|
|
558
|
-
// Memory bomb / consuming all memory
|
|
559
|
-
{ pattern: /\bwhile\s*\(\s*true\s*\)\s*;\s*do\s+\w+\s*=\s*\$\w+\$\w+/, message: 'Potential memory bomb detected' },
|
|
560
|
-
// Overwriting critical boot files
|
|
561
|
-
{ pattern: />\s*\/boot\//, message: 'Cannot write to /boot' },
|
|
562
|
-
{ pattern: /\brm\s+(-[rfvI]+\s+)*\/boot/, message: 'Cannot delete /boot' },
|
|
563
|
-
// System destruction via mv
|
|
564
|
-
{ pattern: /\bmv\s+\/\s+/, message: 'Cannot move root filesystem' },
|
|
565
|
-
{ pattern: /\bmv\s+(-[a-zA-Z]+\s+)*\/\s+/, message: 'Cannot move root filesystem' },
|
|
566
|
-
];
|
|
567
|
-
/**
|
|
568
|
-
* Check if a command matches any critical pattern.
|
|
569
|
-
* Returns the matching pattern info if found, null otherwise.
|
|
570
|
-
*/
|
|
571
|
-
function matchesCriticalPattern(input) {
|
|
572
|
-
for (const { pattern, message } of CRITICAL_PATTERNS) {
|
|
573
|
-
if (pattern.test(input)) {
|
|
574
|
-
return { pattern, message };
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
return null;
|
|
578
|
-
}
|
|
579
|
-
/**
|
|
580
|
-
* Check if a command argument represents a dangerous path.
|
|
581
|
-
*/
|
|
582
|
-
function isDangerousPath(arg) {
|
|
583
|
-
// Root path
|
|
584
|
-
if (arg === '/' || arg === '/*')
|
|
585
|
-
return true;
|
|
586
|
-
// Device paths
|
|
587
|
-
if (arg.startsWith('/dev/'))
|
|
588
|
-
return true;
|
|
589
|
-
// System directories
|
|
590
|
-
const systemPaths = ['/etc', '/bin', '/sbin', '/usr', '/boot', '/lib', '/lib64', '/var', '/sys', '/proc'];
|
|
591
|
-
for (const path of systemPaths) {
|
|
592
|
-
if (arg === path || arg.startsWith(path + '/'))
|
|
593
|
-
return true;
|
|
594
|
-
}
|
|
595
|
-
// Home directory
|
|
596
|
-
if (arg === '~' || arg === '$HOME')
|
|
597
|
-
return true;
|
|
598
|
-
return false;
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* Check if a redirect targets a dangerous path.
|
|
602
|
-
*/
|
|
603
|
-
function isDangerousRedirect(redirect) {
|
|
604
|
-
const target = redirect.target.value;
|
|
605
|
-
// Device redirects
|
|
606
|
-
if (target.startsWith('/dev/'))
|
|
607
|
-
return true;
|
|
608
|
-
// System file redirects
|
|
609
|
-
if (target.startsWith('/etc/'))
|
|
610
|
-
return true;
|
|
611
|
-
return false;
|
|
612
|
-
}
|
|
613
|
-
/**
|
|
614
|
-
* Extract all commands from an AST.
|
|
615
|
-
*/
|
|
616
|
-
function extractCommands(ast) {
|
|
617
|
-
const commands = [];
|
|
618
|
-
function visit(node) {
|
|
619
|
-
switch (node.type) {
|
|
620
|
-
case 'command':
|
|
621
|
-
if (node.name.value) {
|
|
622
|
-
// Handle paths like /usr/bin/rm
|
|
623
|
-
const name = node.name.value.split('/').pop() ?? node.name.value;
|
|
624
|
-
commands.push(name);
|
|
625
|
-
}
|
|
626
|
-
break;
|
|
627
|
-
case 'pipeline':
|
|
628
|
-
node.commands.forEach(visit);
|
|
629
|
-
break;
|
|
630
|
-
case 'list':
|
|
631
|
-
node.pipelines.forEach(visit);
|
|
632
|
-
break;
|
|
633
|
-
case 'subshell':
|
|
634
|
-
visit(node.body);
|
|
635
|
-
break;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
visit(ast);
|
|
639
|
-
return commands;
|
|
640
|
-
}
|
|
641
|
-
/**
|
|
642
|
-
* Analyze AST for safety issues.
|
|
643
|
-
*/
|
|
644
|
-
function findSafetyIssues(ast, blockedCommands = new Set(), originalInput) {
|
|
645
|
-
const issues = [];
|
|
646
|
-
function visit(node) {
|
|
647
|
-
switch (node.type) {
|
|
648
|
-
case 'command': {
|
|
649
|
-
const cmdName = node.name.value.split('/').pop() ?? node.name.value;
|
|
650
|
-
// Check blocked commands
|
|
651
|
-
if (blockedCommands.has(cmdName)) {
|
|
652
|
-
issues.push({
|
|
653
|
-
type: 'blocked_command',
|
|
654
|
-
message: `Command '${cmdName}' is blocked`,
|
|
655
|
-
severity: 'critical',
|
|
656
|
-
start: node.start,
|
|
657
|
-
end: node.end,
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
// Check dangerous commands
|
|
661
|
-
if (DANGEROUS_COMMANDS.has(cmdName)) {
|
|
662
|
-
issues.push({
|
|
663
|
-
type: 'dangerous_command',
|
|
664
|
-
message: `Command '${cmdName}' is potentially dangerous`,
|
|
665
|
-
severity: 'high',
|
|
666
|
-
start: node.start,
|
|
667
|
-
end: node.end,
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
// Check rm with dangerous flags/paths
|
|
671
|
-
if (cmdName === 'rm') {
|
|
672
|
-
const hasForceFlags = node.args.some(arg => arg.value === '-rf' || arg.value === '-fr' ||
|
|
673
|
-
arg.value.includes('r') && arg.value.includes('f') && arg.value.startsWith('-'));
|
|
674
|
-
const hasDangerousPath = node.args.some(arg => isDangerousPath(arg.value));
|
|
675
|
-
const hasWildcard = node.args.some(arg => arg.value.includes('*'));
|
|
676
|
-
const hasRootPath = node.args.some(arg => arg.value === '/' || arg.value === '/*');
|
|
677
|
-
const hasNoPreserveRoot = node.args.some(arg => arg.value === '--no-preserve-root');
|
|
678
|
-
// Critical: rm targeting root or with --no-preserve-root
|
|
679
|
-
if ((hasForceFlags && hasRootPath) || hasNoPreserveRoot) {
|
|
680
|
-
issues.push({
|
|
681
|
-
type: 'critical_pattern',
|
|
682
|
-
message: `rm targeting root filesystem is always blocked`,
|
|
683
|
-
severity: 'critical',
|
|
684
|
-
critical: true,
|
|
685
|
-
start: node.start,
|
|
686
|
-
end: node.end,
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
else if ((hasForceFlags && hasDangerousPath) || (hasForceFlags && hasWildcard)) {
|
|
690
|
-
issues.push({
|
|
691
|
-
type: 'data_destruction',
|
|
692
|
-
message: `rm with recursive/force flags targeting dangerous path`,
|
|
693
|
-
severity: 'critical',
|
|
694
|
-
start: node.start,
|
|
695
|
-
end: node.end,
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
// Check dd to device
|
|
700
|
-
if (cmdName === 'dd') {
|
|
701
|
-
const ofArg = node.args.find(arg => arg.value.startsWith('of='));
|
|
702
|
-
if (ofArg && ofArg.value.includes('/dev/')) {
|
|
703
|
-
// Check if it's writing to a block device (critical) vs a safe device like /dev/null
|
|
704
|
-
const isSafeDevice = ['/dev/null', '/dev/zero', '/dev/random', '/dev/urandom'].some(d => ofArg.value.includes(d));
|
|
705
|
-
if (!isSafeDevice) {
|
|
706
|
-
issues.push({
|
|
707
|
-
type: 'critical_pattern',
|
|
708
|
-
message: `dd writing to device is always blocked`,
|
|
709
|
-
severity: 'critical',
|
|
710
|
-
critical: true,
|
|
711
|
-
start: node.start,
|
|
712
|
-
end: node.end,
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
// Check chmod 777
|
|
718
|
-
if (cmdName === 'chmod') {
|
|
719
|
-
if (node.args.some(arg => arg.value === '777')) {
|
|
720
|
-
issues.push({
|
|
721
|
-
type: 'dangerous_pattern',
|
|
722
|
-
message: `chmod 777 makes files world-writable`,
|
|
723
|
-
severity: 'high',
|
|
724
|
-
start: node.start,
|
|
725
|
-
end: node.end,
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
// Check dangerous redirects
|
|
730
|
-
for (const redirect of node.redirects) {
|
|
731
|
-
if (isDangerousRedirect(redirect)) {
|
|
732
|
-
issues.push({
|
|
733
|
-
type: 'data_destruction',
|
|
734
|
-
message: `Redirect to dangerous path: ${redirect.target.value}`,
|
|
735
|
-
severity: 'high',
|
|
736
|
-
start: redirect.start,
|
|
737
|
-
end: redirect.end,
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
case 'pipeline': {
|
|
744
|
-
// Check for curl/wget piped to shell (critical - remote code execution)
|
|
745
|
-
const cmds = node.commands;
|
|
746
|
-
for (let i = 0; i < cmds.length - 1; i++) {
|
|
747
|
-
const current = cmds[i];
|
|
748
|
-
const next = cmds[i + 1];
|
|
749
|
-
if (current.type === 'command' && next.type === 'command') {
|
|
750
|
-
const currentName = current.name.value.split('/').pop();
|
|
751
|
-
const nextName = next.name.value.split('/').pop();
|
|
752
|
-
if ((currentName === 'curl' || currentName === 'wget') &&
|
|
753
|
-
(nextName === 'bash' || nextName === 'sh' || nextName === 'zsh')) {
|
|
754
|
-
issues.push({
|
|
755
|
-
type: 'critical_pattern',
|
|
756
|
-
message: `Piping ${currentName} output to shell is always blocked`,
|
|
757
|
-
severity: 'critical',
|
|
758
|
-
critical: true,
|
|
759
|
-
start: node.start,
|
|
760
|
-
end: node.end,
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
node.commands.forEach(visit);
|
|
766
|
-
break;
|
|
767
|
-
}
|
|
768
|
-
case 'list':
|
|
769
|
-
node.pipelines.forEach(visit);
|
|
770
|
-
break;
|
|
771
|
-
case 'subshell':
|
|
772
|
-
visit(node.body);
|
|
773
|
-
break;
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
visit(ast);
|
|
777
|
-
// Check for critical patterns in the original input
|
|
778
|
-
const textToCheck = originalInput ?? ast.raw;
|
|
779
|
-
const criticalMatch = matchesCriticalPattern(textToCheck);
|
|
780
|
-
if (criticalMatch) {
|
|
781
|
-
issues.push({
|
|
782
|
-
type: 'critical_pattern',
|
|
783
|
-
message: criticalMatch.message,
|
|
784
|
-
severity: 'critical',
|
|
785
|
-
critical: true,
|
|
786
|
-
start: 0,
|
|
787
|
-
end: textToCheck.length,
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
return issues;
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* Determine impact level from issues.
|
|
794
|
-
*/
|
|
795
|
-
function determineImpact(issues, commands) {
|
|
796
|
-
// Check issues first
|
|
797
|
-
if (issues.some(i => i.severity === 'critical'))
|
|
798
|
-
return 'critical';
|
|
799
|
-
if (issues.some(i => i.severity === 'high'))
|
|
800
|
-
return 'high';
|
|
801
|
-
if (issues.some(i => i.severity === 'medium'))
|
|
802
|
-
return 'medium';
|
|
803
|
-
if (issues.some(i => i.severity === 'low'))
|
|
804
|
-
return 'low';
|
|
805
|
-
// Check command types
|
|
806
|
-
for (const cmd of commands) {
|
|
807
|
-
if (DANGEROUS_COMMANDS.has(cmd))
|
|
808
|
-
return 'high';
|
|
809
|
-
if (MEDIUM_IMPACT_COMMANDS.has(cmd))
|
|
810
|
-
return 'medium';
|
|
811
|
-
if (LOW_IMPACT_COMMANDS.has(cmd))
|
|
812
|
-
return 'low';
|
|
813
|
-
}
|
|
814
|
-
// All safe commands
|
|
815
|
-
if (commands.every(cmd => SAFE_COMMANDS.has(cmd)))
|
|
816
|
-
return 'none';
|
|
817
|
-
return 'none';
|
|
818
|
-
}
|
|
819
|
-
/**
|
|
820
|
-
* Determine safety level from issues.
|
|
821
|
-
* - 'critical': Has issues marked as critical (cannot be executed even with confirm)
|
|
822
|
-
* - 'dangerous': Has issues but none are critical (can be executed with confirm)
|
|
823
|
-
* - 'safe': No issues found
|
|
824
|
-
*/
|
|
825
|
-
function determineSafetyLevel(issues) {
|
|
826
|
-
if (issues.some(i => i.critical === true)) {
|
|
827
|
-
return 'critical';
|
|
828
|
-
}
|
|
829
|
-
if (issues.length > 0) {
|
|
830
|
-
return 'dangerous';
|
|
831
|
-
}
|
|
832
|
-
return 'safe';
|
|
833
|
-
}
|
|
834
|
-
/**
|
|
835
|
-
* Analyze a bash command AST for safety.
|
|
836
|
-
*
|
|
837
|
-
* @param ast - The parsed AST to analyze
|
|
838
|
-
* @param blockedCommands - Set of commands that are blocked
|
|
839
|
-
* @param originalInput - Original input string for pattern matching
|
|
840
|
-
* @returns Safety analysis result
|
|
841
|
-
*
|
|
842
|
-
* @example
|
|
843
|
-
* ```typescript
|
|
844
|
-
* const ast = parseBashCommand('rm -rf /')
|
|
845
|
-
* const safety = analyzeASTSafety(ast)
|
|
846
|
-
* if (safety.dangerous) {
|
|
847
|
-
* console.log(`Blocked: ${safety.reason}`)
|
|
848
|
-
* }
|
|
849
|
-
* ```
|
|
850
|
-
*/
|
|
851
|
-
export function analyzeASTSafety(ast, blockedCommands = new Set(), originalInput) {
|
|
852
|
-
const commands = extractCommands(ast);
|
|
853
|
-
const issues = findSafetyIssues(ast, blockedCommands, originalInput);
|
|
854
|
-
const impact = determineImpact(issues, commands);
|
|
855
|
-
const safetyLevel = determineSafetyLevel(issues);
|
|
856
|
-
const dangerous = issues.length > 0;
|
|
857
|
-
return {
|
|
858
|
-
dangerous,
|
|
859
|
-
safetyLevel,
|
|
860
|
-
reason: issues.length > 0 ? issues[0].message : undefined,
|
|
861
|
-
commands,
|
|
862
|
-
impact,
|
|
863
|
-
issues,
|
|
864
|
-
ast,
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
/**
|
|
868
|
-
* Parse and analyze a bash command string for safety.
|
|
869
|
-
*
|
|
870
|
-
* This is a convenience function that combines parsing and analysis.
|
|
871
|
-
*
|
|
872
|
-
* @param input - The bash command string to analyze
|
|
873
|
-
* @param blockedCommands - Set of commands that are blocked
|
|
874
|
-
* @returns Safety analysis result
|
|
875
|
-
*
|
|
876
|
-
* @example
|
|
877
|
-
* ```typescript
|
|
878
|
-
* const result = parseAndAnalyze('rm -rf /')
|
|
879
|
-
* if (result.dangerous) {
|
|
880
|
-
* console.log(`Command blocked: ${result.reason}`)
|
|
881
|
-
* }
|
|
882
|
-
* ```
|
|
883
|
-
*/
|
|
884
|
-
export function parseAndAnalyze(input, blockedCommands = new Set()) {
|
|
885
|
-
const ast = parseBashCommand(input);
|
|
886
|
-
return analyzeASTSafety(ast, blockedCommands, input);
|
|
887
|
-
}
|
|
888
|
-
//# sourceMappingURL=bash-ast.js.map
|