bash-classify 0.9.0__tar.gz → 0.10.0__tar.gz
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.
- {bash_classify-0.9.0 → bash_classify-0.10.0}/.claude-plugin/marketplace.json +1 -1
- {bash_classify-0.9.0 → bash_classify-0.10.0}/PKG-INFO +97 -2
- {bash_classify-0.9.0 → bash_classify-0.10.0}/README.md +95 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/SPEC.md +148 -13
- {bash_classify-0.9.0 → bash_classify-0.10.0}/coding-agent-plugins/claude-code/.claude-plugin/plugin.json +1 -1
- {bash_classify-0.9.0 → bash_classify-0.10.0}/docs/classification-guidance.md +33 -5
- {bash_classify-0.9.0 → bash_classify-0.10.0}/schemas/command.schema.json +7 -1
- bash_classify-0.10.0/schemas/match-rules.schema.json +57 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/__init__.py +9 -1
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/classifier.py +30 -2
- bash_classify-0.10.0/src/bash_classify/cli.py +250 -0
- bash_classify-0.10.0/src/bash_classify/commands/eval.yaml +8 -0
- bash_classify-0.10.0/src/bash_classify/commands/exec.yaml +12 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/git.yaml +37 -4
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/glab.yaml +65 -0
- bash_classify-0.10.0/src/bash_classify/commands/snip.yaml +79 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/matcher.py +95 -13
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/models.py +5 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/parser.py +118 -2
- bash_classify-0.10.0/src/bash_classify/rules.py +248 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/api-merge-request-award-emoji.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/api-merge-request-include-diverged.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/api-merge-request-itself.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/api-merge-request-pipelines-list.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/api-merge-request-update-description.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/api-project-lookup.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/ci-list.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/comment-only-mention.sh +2 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/echo-mentions-api-endpoint.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/echo-mentions-command.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/git-commit-heredoc-message.sh +8 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/git-commit-inline-message.sh +3 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/glab-discussion-read-and-api-mr.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/glab-discussion-read.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/glab-pipeline-inspect.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/grep-pattern-mentions-command.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/grep-recursive-for-command.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/heredoc-brief-2026-09-04.sh +88 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/heredoc-brief-mentions-blocked-commands.sh +14 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/heredoc-report-mentions-blocked.sh +10 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/heredoc-skill-custom-delimiter.sh +11 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/heredoc-skill-phrase-split-across-lines.sh +10 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/hook-self-test-json-payload.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/loop-over-quoted-command-strings.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/mr-note-list-output-json.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/mr-note-list.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/mr-update-then-view.sh +3 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/mr-view-bare.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/mr-view-grep-pipeline.sh +2 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/mr-view-output-equals-json.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/mr-view-output-json.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/mr-view-output-short-flag.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/printf-mentions-command.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/python-heredoc-triple-quoted-mention.sh +15 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/allow/ripgrep-pattern-args.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-absolute-binary-path.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-delete-note-nested-api.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-cd-and-prefix.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-export-host-loop.sh +8 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-for-loop.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-heredoc-then-call.sh +8 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-hostname-python-consumer.sh +9 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-line-continuation.sh +2 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-method-get-field.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-output-ndjson.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-paginate-before-path.sh +3 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-paginate.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-per-page-query.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-quoted-paginate-jq.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-redirect-to-file.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-repo-flag-first.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-discussions-var-capture.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-edit-note-put-heredoc.sh +7 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-new-diff-note-input-stdin.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-new-diff-note-position-fields.sh +6 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-note-award-emoji.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-note-by-id-or-retry.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-notes-after-allowed-mr-view.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-notes-query-sort.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-notes-timeout-prefix.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-reply-method-post-after-path.sh +6 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-reply-post-body-from-file.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-reply-post-heredoc-body.sh +8 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-resolve-loop.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-resolve-put-field.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/api-single-discussion-by-id.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/heredoc-then-and-chain-to-blocked.sh +3 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/heredoc-then-pipe-to-blocked.sh +3 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-note-custom-heredoc-delimiter.sh +8 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-note-help.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-note-heredoc-body.sh +7 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-note-inline-after-mr-update.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-note-long-message-flag.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-note-repo-before-message.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-note-second-in-pipeline.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-note-then-close.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-view-comments-bare.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-view-comments-loop.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-view-comments-no-iid.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-view-comments-page-flag.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-view-comments-repo-flag.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-view-comments-short-cluster.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/mr-view-comments-short-flag.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/wrapper-bash-c.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/block/wrapper-sudo.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-discussion/rules.yaml +17 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/api-job-metadata-without-trace.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/api-job-metadata-xargs.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/api-merge-request-itself.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/api-merge-request-pipelines-list-per-page.sh +5 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/api-merge-request-pipelines-list.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/api-pipelines-list-bash-c.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/api-pipelines-list-ref.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/api-pipelines-list-single-quoted.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/ci-lint-local-file.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/ci-list-per-page.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/ci-list.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/ci-status-compact.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/ci-status-pipeline-id.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/comment-only-mention.sh +2 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/echo-mentions-ci-command.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/git-commit-heredoc-message.sh +9 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/git-commit-inline-message.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/glab-discussion-read.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/glab-pipeline-inspect.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/grep-pattern-mentions-ci.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/heredoc-brief-mentions-ci-commands.sh +16 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/heredoc-skill-mentions-ci.sh +10 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/loop-over-quoted-command-strings.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/printf-mentions-ci-command.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/allow/ripgrep-pattern-args.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-ci-lint-continuation-lines.sh +5 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-ci-lint-input-and-header.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-ci-lint-method-post-field.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-ci-lint-raw-field-cat.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-job-trace-after-allowed-metadata.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-job-trace-env-assignment.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-job-trace-redirect-to-file.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-job-trace-retry-loop.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-job-trace-xargs.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-detail.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-env-assignment.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-hostname-after-path.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-jobs-comment-and-loop.sh +5 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-jobs-list.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-jobs-nested-loop-timeout.sh +6 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-jobs-paginate-ndjson.sh +2 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-jobs-python-consumer.sh +6 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-jobs-scope-failed-loop.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-status-poll-loop.sh +6 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-test-report.sh +3 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/ci-get-after-allowed-status.sh +2 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/ci-get-branch-in-echo-chain.sh +3 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/ci-get-branch-ref-sleep-prefix.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/ci-get-json-jq-sort-uniq.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/ci-get-then-ci-trace.sh +4 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/ci-get-var-capture-poll-loop.sh +9 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/ci-get-with-job-details-repo.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/ci-trace-job-name-with-pipeline.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/ci-trace-pipeline-id-before-job-name.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/ci-trace-redirect-to-file.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/ci-trace-repo-before-job-id.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/ci-view-help.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/ci-view-job-web-or-api.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/ci-view-pipeline-id.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/heredoc-then-pipe-to-blocked.sh +3 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/wrapper-bash-c.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/block/wrapper-sudo.sh +1 -0
- bash_classify-0.10.0/tests/fixtures/match/glab-pipeline/rules.yaml +26 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/tests/test_classifier.py +130 -1
- bash_classify-0.10.0/tests/test_cli.py +255 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/tests/test_cli_serialization.py +64 -0
- bash_classify-0.10.0/tests/test_match_fixtures.py +176 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/tests/test_matcher.py +225 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/tests/test_parser.py +98 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/tests/test_real_world.py +140 -0
- bash_classify-0.10.0/tests/test_rules.py +443 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/tests/test_schema.py +19 -0
- bash_classify-0.9.0/src/bash_classify/cli.py +0 -149
- bash_classify-0.9.0/tests/test_cli.py +0 -82
- {bash_classify-0.9.0 → bash_classify-0.10.0}/.claude/rules/command-database.md +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/.editorconfig +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/.github/workflows/ci.yml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/.github/workflows/publish.yml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/.gitignore +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/.python-version +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/CLAUDE.md +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/LICENSE +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/coding-agent-plugins/claude-code/hooks/hook-permission-request.sh +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/coding-agent-plugins/claude-code/hooks/hook-pre-tool-use.sh +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/coding-agent-plugins/claude-code/hooks/hooks.json +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/pyproject.toml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/ruff.toml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/__main__.py +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/agent-browser.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/apt.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/awk.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/basename.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/bash-classify.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/bash.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/black.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/brew.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/builtin.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/cargo.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/cat.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/chgrp.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/chmod.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/chown.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/column.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/comm.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/command.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/cp.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/curl.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/cut.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/date.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/df.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/diff.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/dig.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/dirname.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/docker.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/du.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/echo.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/env.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/eslint.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/expand.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/false.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/file.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/find.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/fmt.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/fold.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/free.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/gawk.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/gcloud.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/gh.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/glab-discussion.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/go.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/gog.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/gradle.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/gradlew.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/grep.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/groups.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/gunzip.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/gws.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/gzip.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/hadolint.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/head.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/helm.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/hostname.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/id.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/install.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/isort.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/java.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/javac.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/join.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/journalctl.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/jq.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/kill.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/killall.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/kubectl.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/less.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/ln.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/ls.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/make.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/md5sum.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/mise.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/mkdir.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/more.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/mv.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/mvn.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/mvnw.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/mypy.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/netstat.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/nice.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/nl.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/node.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/nohup.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/npm.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/npx.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/nslookup.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/paste.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/ping.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/pip.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/pip3.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/pipx.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/pkill.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/pnpm.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/poetry.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/prettier.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/printenv.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/printf.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/ps.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/pup.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/pwd.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/pytest.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/python.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/python3.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/read.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/readlink.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/realpath.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/rev.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/rg.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/rm.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/rsync.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/rtk.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/ruff.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/scp.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/sed.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/seq.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/sh.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/sha1sum.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/sha256sum.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/shellcheck.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/slack.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/sleep.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/sort.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/ss.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/ssh.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/stat.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/stdbuf.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/strace.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/strings.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/sudo.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/systemctl.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/tac.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/tail.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/tar.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/tee.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/terraform.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/test.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/time.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/timeout.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/top.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/touch.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/tr.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/traceroute.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/true.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/tsc.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/type.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/uname.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/unexpand.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/uniq.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/unzip.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/uptime.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/uv.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/wait.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/wc.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/wget.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/whereis.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/which.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/whoami.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/xargs.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/xmllint.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/yarn.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/yes.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/yq.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/zip.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/commands/zsh.yaml +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/database.py +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/src/bash_classify/py.typed +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/tests/__init__.py +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/tests/conftest.py +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/tests/test_database.py +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/tests/test_models.py +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/tests/test_smoke.py +0 -0
- {bash_classify-0.9.0 → bash_classify-0.10.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: bash-classify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Classify bash commands as READONLY, WRITE, DANGEROUS, or UNKNOWN
|
|
5
5
|
Project-URL: Homepage, https://github.com/fprochazka/bash-classify
|
|
6
6
|
Project-URL: Repository, https://github.com/fprochazka/bash-classify
|
|
@@ -61,6 +61,66 @@ $ echo 'find . -name "*.pyc" -delete' | bash-classify | jq '.classification'
|
|
|
61
61
|
"DANGEROUS"
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
## Matching command shapes
|
|
65
|
+
|
|
66
|
+
Classification answers "how risky is this?". A deny hook usually has a narrower question:
|
|
67
|
+
"does this expression run command shape X?" `bash-classify match` answers that one. It
|
|
68
|
+
parses the expression, walks every invocation at every depth, and reports which of the
|
|
69
|
+
shapes in a rules file were actually invoked — so a heredoc body, an `echo` string, a
|
|
70
|
+
`#` comment or a `grep` pattern that merely *names* the command does not count.
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
# blocked-commands.yaml
|
|
74
|
+
rules:
|
|
75
|
+
- name: mr-discussions-api
|
|
76
|
+
command: [glab, api]
|
|
77
|
+
any_arg_matches: 'merge_requests/[^/?]+/(discussions|notes)(/|\?|$)'
|
|
78
|
+
|
|
79
|
+
- name: mr-view-comments
|
|
80
|
+
command: [glab, mr, view]
|
|
81
|
+
any_option: [--comments, -c]
|
|
82
|
+
|
|
83
|
+
- name: mr-note
|
|
84
|
+
command: [glab, mr, note]
|
|
85
|
+
except: [[glab, mr, note, list]]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
$ echo 'sudo glab mr note 42 -m hi' | bash-classify match --rules blocked-commands.yaml
|
|
90
|
+
{
|
|
91
|
+
"matches": [
|
|
92
|
+
{
|
|
93
|
+
"rule": "mr-note",
|
|
94
|
+
"command": ["glab", "mr", "note"],
|
|
95
|
+
"argv": ["glab", "mr", "note", "42", "-m", "hi"],
|
|
96
|
+
"via": ["sudo"]
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"parse_warnings": []
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Within one rule every condition given must hold; rules are independent of each other, and
|
|
104
|
+
one invocation can match several. `command` is a prefix match against the *resolved*
|
|
105
|
+
command path, so `/usr/bin/glab --repo x mr note` still resolves to `glab mr note`.
|
|
106
|
+
`any_option` looks at the options actually present, with values stripped and declared
|
|
107
|
+
clusters expanded (`-wc` carries `-c`). `any_arg_matches` is a Python `re.search` over
|
|
108
|
+
every argument token — a pattern written for `grep -E` needs `\S` rather than
|
|
109
|
+
`[^[:space:]]`. `via` lists the enclosing wrappers, outermost first.
|
|
110
|
+
|
|
111
|
+
**Two things a caller has to check.** First, `parse_warnings` is always present: when it
|
|
112
|
+
is non-empty the expression could not be fully parsed, so an empty `matches` proves nothing
|
|
113
|
+
and the caller should fall back to whatever it did before. Second, check that the output
|
|
114
|
+
actually has a `matches` key. A `bash-classify` older than this mode does not reject the
|
|
115
|
+
unknown `match` argument — it ignores it, classifies stdin and exits 0, so the caller gets
|
|
116
|
+
a normal classification JSON with no `matches` key. Treat a non-zero exit, unparseable
|
|
117
|
+
output, or output without a `matches` key as "cannot answer" and fall back; never read a
|
|
118
|
+
missing `matches` as "nothing matched".
|
|
119
|
+
|
|
120
|
+
Exit codes are `0` whether or not anything matched, `1` for empty input or no input within
|
|
121
|
+
5 seconds, and `2` for bad arguments, an unreadable or invalid rules file, or an internal
|
|
122
|
+
error.
|
|
123
|
+
|
|
64
124
|
## Claude Code plugin
|
|
65
125
|
|
|
66
126
|
The repo includes a Claude Code plugin that auto-allows low-risk bash commands via a `PreToolUse` hook.
|
|
@@ -134,6 +194,41 @@ print(result.classification) # Classification.READONLY
|
|
|
134
194
|
print(result.risk) # Risk.LOW
|
|
135
195
|
```
|
|
136
196
|
|
|
197
|
+
Each command result carries the options it actually uses and the positionals left after parsing. Option values
|
|
198
|
+
are stripped, so `--key=value` shows up as `--key` and `-fvalue` as `-f`:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
command = classify_expression("git commit --amend -m 'wip'").commands[0]
|
|
202
|
+
print(command.command) # ['git', 'commit']
|
|
203
|
+
print(command.options) # ['--amend', '-m']
|
|
204
|
+
print(command.positionals) # []
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`iter_invocations` walks every invocation in an expression depth-first — top-level commands and, recursively,
|
|
208
|
+
the inner commands that wrappers such as `sudo`, `timeout` or `bash -c` delegate to. It yields each invocation
|
|
209
|
+
with its `via` chain: the enclosing wrappers, outermost first, empty at the top level.
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from bash_classify import classify_expression, iter_invocations
|
|
213
|
+
|
|
214
|
+
for invocation, via in iter_invocations(classify_expression("sudo timeout 5 ls")):
|
|
215
|
+
print(via, invocation.command)
|
|
216
|
+
# [] ['sudo']
|
|
217
|
+
# ['sudo'] ['timeout']
|
|
218
|
+
# ['sudo', 'timeout'] ['ls']
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`load_rules` and `match_expression` are the same thing from Python:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from bash_classify import load_rules, match_expression
|
|
225
|
+
|
|
226
|
+
rules = load_rules("blocked-commands.yaml")
|
|
227
|
+
result = match_expression('cat > brief.md <<"EOF"\nmentions glab mr note\nEOF', rules)
|
|
228
|
+
print(result.matches) # [] - the heredoc body is data, not a command
|
|
229
|
+
print(result.parse_warnings) # []
|
|
230
|
+
```
|
|
231
|
+
|
|
137
232
|
See [SPEC.md](SPEC.md) for the full specification.
|
|
138
233
|
|
|
139
234
|
## Development
|
|
@@ -32,6 +32,66 @@ $ echo 'find . -name "*.pyc" -delete' | bash-classify | jq '.classification'
|
|
|
32
32
|
"DANGEROUS"
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
## Matching command shapes
|
|
36
|
+
|
|
37
|
+
Classification answers "how risky is this?". A deny hook usually has a narrower question:
|
|
38
|
+
"does this expression run command shape X?" `bash-classify match` answers that one. It
|
|
39
|
+
parses the expression, walks every invocation at every depth, and reports which of the
|
|
40
|
+
shapes in a rules file were actually invoked — so a heredoc body, an `echo` string, a
|
|
41
|
+
`#` comment or a `grep` pattern that merely *names* the command does not count.
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
# blocked-commands.yaml
|
|
45
|
+
rules:
|
|
46
|
+
- name: mr-discussions-api
|
|
47
|
+
command: [glab, api]
|
|
48
|
+
any_arg_matches: 'merge_requests/[^/?]+/(discussions|notes)(/|\?|$)'
|
|
49
|
+
|
|
50
|
+
- name: mr-view-comments
|
|
51
|
+
command: [glab, mr, view]
|
|
52
|
+
any_option: [--comments, -c]
|
|
53
|
+
|
|
54
|
+
- name: mr-note
|
|
55
|
+
command: [glab, mr, note]
|
|
56
|
+
except: [[glab, mr, note, list]]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
$ echo 'sudo glab mr note 42 -m hi' | bash-classify match --rules blocked-commands.yaml
|
|
61
|
+
{
|
|
62
|
+
"matches": [
|
|
63
|
+
{
|
|
64
|
+
"rule": "mr-note",
|
|
65
|
+
"command": ["glab", "mr", "note"],
|
|
66
|
+
"argv": ["glab", "mr", "note", "42", "-m", "hi"],
|
|
67
|
+
"via": ["sudo"]
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"parse_warnings": []
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Within one rule every condition given must hold; rules are independent of each other, and
|
|
75
|
+
one invocation can match several. `command` is a prefix match against the *resolved*
|
|
76
|
+
command path, so `/usr/bin/glab --repo x mr note` still resolves to `glab mr note`.
|
|
77
|
+
`any_option` looks at the options actually present, with values stripped and declared
|
|
78
|
+
clusters expanded (`-wc` carries `-c`). `any_arg_matches` is a Python `re.search` over
|
|
79
|
+
every argument token — a pattern written for `grep -E` needs `\S` rather than
|
|
80
|
+
`[^[:space:]]`. `via` lists the enclosing wrappers, outermost first.
|
|
81
|
+
|
|
82
|
+
**Two things a caller has to check.** First, `parse_warnings` is always present: when it
|
|
83
|
+
is non-empty the expression could not be fully parsed, so an empty `matches` proves nothing
|
|
84
|
+
and the caller should fall back to whatever it did before. Second, check that the output
|
|
85
|
+
actually has a `matches` key. A `bash-classify` older than this mode does not reject the
|
|
86
|
+
unknown `match` argument — it ignores it, classifies stdin and exits 0, so the caller gets
|
|
87
|
+
a normal classification JSON with no `matches` key. Treat a non-zero exit, unparseable
|
|
88
|
+
output, or output without a `matches` key as "cannot answer" and fall back; never read a
|
|
89
|
+
missing `matches` as "nothing matched".
|
|
90
|
+
|
|
91
|
+
Exit codes are `0` whether or not anything matched, `1` for empty input or no input within
|
|
92
|
+
5 seconds, and `2` for bad arguments, an unreadable or invalid rules file, or an internal
|
|
93
|
+
error.
|
|
94
|
+
|
|
35
95
|
## Claude Code plugin
|
|
36
96
|
|
|
37
97
|
The repo includes a Claude Code plugin that auto-allows low-risk bash commands via a `PreToolUse` hook.
|
|
@@ -105,6 +165,41 @@ print(result.classification) # Classification.READONLY
|
|
|
105
165
|
print(result.risk) # Risk.LOW
|
|
106
166
|
```
|
|
107
167
|
|
|
168
|
+
Each command result carries the options it actually uses and the positionals left after parsing. Option values
|
|
169
|
+
are stripped, so `--key=value` shows up as `--key` and `-fvalue` as `-f`:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
command = classify_expression("git commit --amend -m 'wip'").commands[0]
|
|
173
|
+
print(command.command) # ['git', 'commit']
|
|
174
|
+
print(command.options) # ['--amend', '-m']
|
|
175
|
+
print(command.positionals) # []
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`iter_invocations` walks every invocation in an expression depth-first — top-level commands and, recursively,
|
|
179
|
+
the inner commands that wrappers such as `sudo`, `timeout` or `bash -c` delegate to. It yields each invocation
|
|
180
|
+
with its `via` chain: the enclosing wrappers, outermost first, empty at the top level.
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from bash_classify import classify_expression, iter_invocations
|
|
184
|
+
|
|
185
|
+
for invocation, via in iter_invocations(classify_expression("sudo timeout 5 ls")):
|
|
186
|
+
print(via, invocation.command)
|
|
187
|
+
# [] ['sudo']
|
|
188
|
+
# ['sudo'] ['timeout']
|
|
189
|
+
# ['sudo', 'timeout'] ['ls']
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`load_rules` and `match_expression` are the same thing from Python:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from bash_classify import load_rules, match_expression
|
|
196
|
+
|
|
197
|
+
rules = load_rules("blocked-commands.yaml")
|
|
198
|
+
result = match_expression('cat > brief.md <<"EOF"\nmentions glab mr note\nEOF', rules)
|
|
199
|
+
print(result.matches) # [] - the heredoc body is data, not a command
|
|
200
|
+
print(result.parse_warnings) # []
|
|
201
|
+
```
|
|
202
|
+
|
|
108
203
|
See [SPEC.md](SPEC.md) for the full specification.
|
|
109
204
|
|
|
110
205
|
## Development
|
|
@@ -186,12 +186,90 @@ $ echo 'sh -c "ls /tmp | grep log"' | bash-classify
|
|
|
186
186
|
}
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
+
### match mode
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
$ echo 'sudo glab mr note 42 -m hi' | bash-classify match --rules blocked-commands.yaml
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"matches": [
|
|
198
|
+
{
|
|
199
|
+
"rule": "mr-note",
|
|
200
|
+
"command": ["glab", "mr", "note"],
|
|
201
|
+
"argv": ["glab", "mr", "note", "42", "-m", "hi"],
|
|
202
|
+
"via": ["sudo"]
|
|
203
|
+
}
|
|
204
|
+
],
|
|
205
|
+
"parse_warnings": []
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`match` answers a narrower question than classification: which of the command shapes
|
|
210
|
+
declared in a rules file does this expression actually invoke? Shell text that only names
|
|
211
|
+
a command — a heredoc body, an `echo` or `printf` argument, a `#` comment, a
|
|
212
|
+
`git commit -m` message, a `grep` pattern — is not an invocation and does not match.
|
|
213
|
+
|
|
214
|
+
#### The rules file
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
rules:
|
|
218
|
+
- name: mr-note # required, unique within the file
|
|
219
|
+
command: [glab, mr, note] # required; prefix match on the resolved path
|
|
220
|
+
except: [[glab, mr, note, list]] # optional; prefix paths excluded from the match
|
|
221
|
+
any_option: [--comments, -c] # optional; at least one of these is present
|
|
222
|
+
any_arg_matches: 'merge_requests/[^/?]+/notes' # optional; Python re.search
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Within one rule every condition that is given must hold (AND). Rules are independent of
|
|
226
|
+
each other (OR), and one invocation may match several rules; each match is reported.
|
|
227
|
+
|
|
228
|
+
| Field | Meaning |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `name` | Reported back on every match; must be unique within the file |
|
|
231
|
+
| `command` | Prefix match against `command` — the *resolved* path, so `/usr/bin/glab --repo x mr note` matches `[glab, mr, note]` |
|
|
232
|
+
| `except` | Prefix paths excluded from the match, so `[glab, mr, note]` can spare `glab mr note list` |
|
|
233
|
+
| `any_option` | At least one of these must appear in `options`: as typed, values stripped, `--key=value` counting as `--key` and a declared cluster `-wc` counting as `-w` and `-c`. Tokens after `--` are positionals and never match |
|
|
234
|
+
| `any_arg_matches` | Python `re.search` applied to every argument token: `argv[1:]` with the resolved subcommand words removed once each. Option flags, option values and positionals are all included; `argv[0]` and the subcommand words are not. A pattern written for `grep -E` needs `\S` instead of `[^[:space:]]` |
|
|
235
|
+
|
|
236
|
+
A rule matches an invocation at any depth: top-level commands and, recursively, the inner
|
|
237
|
+
commands that wrappers delegate to (`sudo`, `timeout`, `xargs`, `bash -c`, `find -exec`,
|
|
238
|
+
`eval`, `exec`). Commands inside `$(...)` are top-level invocations in their own right.
|
|
239
|
+
`via` lists the enclosing wrappers, outermost first, each as its resolved path joined by
|
|
240
|
+
spaces; it is empty at the top level.
|
|
241
|
+
|
|
242
|
+
`parse_warnings` is **always** present in match output, even when empty, because a caller
|
|
243
|
+
has to check it: a non-empty list means the expression could not be fully parsed, so an
|
|
244
|
+
empty `matches` list proves nothing.
|
|
245
|
+
|
|
246
|
+
#### Exit codes for `match`
|
|
247
|
+
|
|
248
|
+
- `0` — ran successfully, whether or not anything matched
|
|
249
|
+
- `1` — empty input, or no input on stdin within 5 seconds
|
|
250
|
+
- `2` — bad arguments, a missing or invalid rules file (the message names the file and the
|
|
251
|
+
offending rule), or an internal error
|
|
252
|
+
|
|
253
|
+
A caller cannot rely on the exit code alone to detect an old binary. Versions before this
|
|
254
|
+
mode parsed `sys.argv` by hand and ignored anything they did not recognize, so
|
|
255
|
+
`bash-classify match --rules f.yaml` on such a binary classifies stdin and exits `0`,
|
|
256
|
+
printing an ordinary classification document with no `matches` key. The check that works is
|
|
257
|
+
the output shape: require a `matches` key, and treat a non-zero exit, unparseable output or
|
|
258
|
+
a missing `matches` key alike as "cannot answer" — fall back rather than reading it as
|
|
259
|
+
"nothing matched".
|
|
260
|
+
|
|
189
261
|
### Exit codes
|
|
190
262
|
|
|
191
263
|
- `0` — successfully classified
|
|
192
|
-
- `1` —
|
|
264
|
+
- `1` — empty input, or no input arrived on stdin within 5 seconds
|
|
193
265
|
- `2` — internal error
|
|
194
266
|
|
|
267
|
+
A bash syntax error does **not** change the exit code. The parse is best-effort: the tool still exits `0`, records the
|
|
268
|
+
problem in `parse_warnings`, and returns whatever commands it could extract from the partial parse. Consumers rely on
|
|
269
|
+
this — check `parse_warnings` to decide whether the `commands` list can be trusted, not the exit code.
|
|
270
|
+
|
|
271
|
+
`match` mode has its own, slightly different set — see [Exit codes for `match`](#exit-codes-for-match) above.
|
|
272
|
+
|
|
195
273
|
### Classification levels
|
|
196
274
|
|
|
197
275
|
| Level | Meaning |
|
|
@@ -299,7 +377,7 @@ The parser walks the CST and extracts a list of `CommandInvocation` objects, eac
|
|
|
299
377
|
| Command substitution `$(cmd)` | Inner command extracted recursively |
|
|
300
378
|
| Process substitution `<(cmd)` / `>(cmd)` | Inner command extracted recursively |
|
|
301
379
|
| Subshells `(cmd)` | Inner command extracted recursively |
|
|
302
|
-
| Heredocs `<<EOF` |
|
|
380
|
+
| Heredocs `<<EOF` | Body is dropped entirely — it is never parsed as commands and never appears in any `argv`; only the operator and the delimiter are recorded as a redirect. Commands written *after* the opener on the same line (`cat <<EOF \| wc -l`, `cat <<EOF && rm x`) are extracted like any other pipeline or list member — tree-sitter nests them inside the heredoc node, but they are real commands. A `;` or `&` in that position is a tree-sitter parse error: the follower cannot be recovered and a `parse_warnings` entry says so |
|
|
303
381
|
| Variable assignments `X=1 cmd` | Prefix assignments stripped, `cmd` extracted |
|
|
304
382
|
| Backgrounding `cmd &` | Classified as EXTERNAL_EFFECTS (side effect: background process) |
|
|
305
383
|
|
|
@@ -307,11 +385,13 @@ The parser walks the CST and extracts a list of `CommandInvocation` objects, eac
|
|
|
307
385
|
|
|
308
386
|
| Construct | Reason |
|
|
309
387
|
|---|---|
|
|
310
|
-
| `eval "..."` | Arbitrary code execution,
|
|
311
|
-
| `
|
|
312
|
-
| `source file` / `. file` | Executes external script |
|
|
388
|
+
| `eval "..."` | Arbitrary code execution. The argument is still parsed and the inner command exposed, but a `min_classification: DANGEROUS` floor keeps both the inner and `eval` itself DANGEROUS |
|
|
389
|
+
| `exec cmd` | Replaces the shell with `cmd`. The inner command is exposed, with the same DANGEROUS floor |
|
|
390
|
+
| `source file` / `. file` | Executes external script, whose contents are never visible |
|
|
313
391
|
| Unquoted variable expansion in command position (`$CMD args`) | Command identity unknown at parse time |
|
|
314
392
|
|
|
393
|
+
`sh` and `bash` are **not** in this table. Their base classification is DANGEROUS, which is what a bare `sh` or `bash` gets, but `-c` is a `flag_value_is_expression` delegation with no `min_classification`: the expression is parsed, the inner commands are exposed, and the wrapper base is erased, so `sh -c "ls"` classifies READONLY.
|
|
394
|
+
|
|
315
395
|
## Command Matching Algorithm
|
|
316
396
|
|
|
317
397
|
This is the core of the tool. Given an `argv` like `["kubectl", "--context=prod", "get", "pods", "-n", "kube-system"]`, the matcher must:
|
|
@@ -841,6 +921,7 @@ The `delegates_to` field defines how a command (or option like `find -exec`) han
|
|
|
841
921
|
| `after_separator` | Everything after the `separator` token forms the inner argv. | `kubectl exec pod -- ls -la` → inner: `["ls", "-la"]` |
|
|
842
922
|
| `terminated_argv` | Tokens after the flag up to `terminator` form the inner argv. `{}` tokens are stripped (they are `find` placeholders). | `find . -exec rm {} \;` → inner: `["rm"]` |
|
|
843
923
|
| `flag_value_is_expression` | The value of the specified `flag` is a complete shell expression string, parsed from scratch through the bash parser (not just tokenized as argv). | `sh -c "ls \| grep foo"` → inner expression: `ls \| grep foo` (two piped commands) |
|
|
924
|
+
| `args_are_expression` | Every token after the command word is joined with single spaces and the result is parsed as a complete shell expression, the way `eval` concatenates its arguments before running them. Option-looking tokens are included, because a command using this mode has no options of its own. Invocations the parser cannot recover yield no inner command. | `eval "glab mr list"` and `eval glab mr list` → inner: `["glab", "mr", "list"]` |
|
|
844
925
|
|
|
845
926
|
#### Delegation fields
|
|
846
927
|
|
|
@@ -909,14 +990,15 @@ Directories are reported as-is (not resolved) since variable expansion may be in
|
|
|
909
990
|
|
|
910
991
|
## Special-cased Commands
|
|
911
992
|
|
|
912
|
-
Most delegation behavior is expressed in the database via `delegates_to`. Only shell builtins that cannot be modeled as regular commands need hardcoded handling
|
|
993
|
+
Most delegation behavior is expressed in the database via `delegates_to`. Only shell builtins that cannot be modeled as regular commands need hardcoded handling — the full set is three short lists in `matcher.py`:
|
|
913
994
|
|
|
914
995
|
| Command | Special handling |
|
|
915
996
|
|---|---|
|
|
916
997
|
| `cd`, `pushd`, `popd` | Directory tracking; classified as `READONLY` |
|
|
917
|
-
| `
|
|
918
|
-
| `source`, `.` | Always `DANGEROUS` — executes external script |
|
|
919
|
-
|
|
998
|
+
| `[`, `[[`, `test` | Always `READONLY` — condition evaluation with no side effects |
|
|
999
|
+
| `source`, `.` | Always `DANGEROUS` — executes an external script whose contents are never visible |
|
|
1000
|
+
|
|
1001
|
+
`eval` and `exec` are **not** in that set. They are ordinary database entries (`commands/eval.yaml`, `commands/exec.yaml`) that delegate: `eval` via `args_are_expression`, `exec` via `rest_are_argv`, each with `min_classification: DANGEROUS`. So their inner command is parsed and reported in `inner_commands`, while the floor keeps both the inner and the wrapper at `DANGEROUS`/`HIGH`.
|
|
920
1002
|
|
|
921
1003
|
Everything else (`sudo`, `env`, `xargs`, `sh -c`, `nice`, `nohup`, `timeout`, `time`, `find -exec`, `kubectl exec --`, etc.) is handled via `delegates_to` in the database — no special code needed.
|
|
922
1004
|
|
|
@@ -930,7 +1012,30 @@ Python 3.12+. Dependencies:
|
|
|
930
1012
|
|
|
931
1013
|
## Output Schema
|
|
932
1014
|
|
|
933
|
-
|
|
1015
|
+
This is the schema of the default mode. `match` mode has its own, much smaller one:
|
|
1016
|
+
|
|
1017
|
+
```json
|
|
1018
|
+
{
|
|
1019
|
+
"matches": [
|
|
1020
|
+
{
|
|
1021
|
+
"rule": "string — the name of the rule that matched",
|
|
1022
|
+
"command": ["string — the resolved command path of the invocation"],
|
|
1023
|
+
"argv": ["string — the full argv of the invocation"],
|
|
1024
|
+
"via": ["string — enclosing wrapper commands, outermost first; empty at top level"]
|
|
1025
|
+
}
|
|
1026
|
+
],
|
|
1027
|
+
"parse_warnings": ["string — non-fatal issues encountered during parsing"]
|
|
1028
|
+
}
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
Both keys are always present in match output, `matches` as `[]` when nothing matched and
|
|
1032
|
+
`parse_warnings` as `[]` on a clean parse. As in the default mode, `parse_warnings` covers
|
|
1033
|
+
nested expressions: a syntax error inside `bash -c "..."` or `eval "..."` surfaces here too.
|
|
1034
|
+
A caller must check both: a non-empty `parse_warnings` means an empty `matches` proves
|
|
1035
|
+
nothing, and a *missing* `matches` key means the output came from a binary that predates
|
|
1036
|
+
this mode.
|
|
1037
|
+
|
|
1038
|
+
The `commands` list below is recursive — any command entry can contain `inner_commands` when the command delegates execution to another command (via `delegates_to` in the database).
|
|
934
1039
|
|
|
935
1040
|
```json
|
|
936
1041
|
{
|
|
@@ -945,6 +1050,8 @@ The `commands` list is recursive — any command entry can contain `inner_comman
|
|
|
945
1050
|
"argv": ["string — full original argv for this command"],
|
|
946
1051
|
"classification": "READONLY | LOCAL_EFFECTS | EXTERNAL_EFFECTS | DANGEROUS | UNKNOWN",
|
|
947
1052
|
"matched_rule": "string — dotted path in database, or null",
|
|
1053
|
+
"options": ["string — every option present, canonical flag only, no values; always present, empty when none"],
|
|
1054
|
+
"positionals": ["string — non-option tokens left after subcommand and option parsing; always present, empty when none"],
|
|
948
1055
|
"ignored_options": ["string — global options that were stripped"],
|
|
949
1056
|
"remaining_options": ["string — options that were not in database"],
|
|
950
1057
|
"classification_reason": "string — why this classification was chosen",
|
|
@@ -959,6 +1066,8 @@ The `commands` list is recursive — any command entry can contain `inner_comman
|
|
|
959
1066
|
"argv": ["..."],
|
|
960
1067
|
"classification": "...",
|
|
961
1068
|
"matched_rule": "...",
|
|
1069
|
+
"options": ["... — same meaning as on the enclosing command"],
|
|
1070
|
+
"positionals": ["... — same meaning as on the enclosing command"],
|
|
962
1071
|
"inner_commands": ["... — recursive, can nest further"]
|
|
963
1072
|
}
|
|
964
1073
|
]
|
|
@@ -971,10 +1080,37 @@ The `commands` list is recursive — any command entry can contain `inner_comman
|
|
|
971
1080
|
"affects_classification": "bool"
|
|
972
1081
|
}
|
|
973
1082
|
],
|
|
974
|
-
"parse_warnings": ["string — non-fatal issues encountered during parsing"]
|
|
1083
|
+
"parse_warnings": ["string — non-fatal issues encountered during parsing. A non-empty list means the parse was best-effort and the commands list may be incomplete."]
|
|
975
1084
|
}
|
|
976
1085
|
```
|
|
977
1086
|
|
|
1087
|
+
`parse_warnings` covers **nested** expressions as well as the outer one. A syntax error inside
|
|
1088
|
+
`bash -c "..."`, `sh -c`, `zsh -c` or `eval "..."` surfaces here at any wrapper depth
|
|
1089
|
+
(`sudo bash -c '...'`, `find . -exec bash -c '...' \;`), and the warning text names the nested
|
|
1090
|
+
expression it came from, so a caller can tell which part of the command could not be read.
|
|
1091
|
+
Identical warnings are reported once.
|
|
1092
|
+
|
|
1093
|
+
#### `options` and `positionals`
|
|
1094
|
+
|
|
1095
|
+
`options` records which options an invocation actually carries, with option values stripped and each option in
|
|
1096
|
+
its canonical form: `--key=value` contributes `--key`, `-fvalue` contributes `-f`, and a cluster whose every
|
|
1097
|
+
character is declared in the database (`-wc`) contributes each character (`-w`, `-c`). Options the database does
|
|
1098
|
+
not model are included too — present means present. Tokens after `--` are positionals and contribute nothing.
|
|
1099
|
+
|
|
1100
|
+
Two consequences of matching against a database that models only part of a real CLI. In a cluster whose first
|
|
1101
|
+
character is declared but a later one is not (`-wx` with only `-w` declared), the undeclared characters are not
|
|
1102
|
+
reported anywhere — not in `options`, not in `remaining_options` — so a check against `options` can miss such an
|
|
1103
|
+
option but can never falsely fire on one. In the other direction, an option the database does not model but that
|
|
1104
|
+
really takes a value has its value reported as a second option (`-e -foo` yields `-e` and `-foo`, because nothing
|
|
1105
|
+
says `-e` consumes the next token), so `options` errs toward over-reporting for unmodelled options. Declaring the
|
|
1106
|
+
option in the database fixes both.
|
|
1107
|
+
|
|
1108
|
+
`positionals` holds the non-option tokens that remain once the subcommand chain and the options have been
|
|
1109
|
+
consumed, including `--` itself and everything after it.
|
|
1110
|
+
|
|
1111
|
+
Both are always serialized, as `[]` when the command has none. Results that never reach option parsing — shell
|
|
1112
|
+
builtins, an unknown binary, an empty argv — carry `null` internally and serialize as `[]`.
|
|
1113
|
+
|
|
978
1114
|
### Recursive classification rules
|
|
979
1115
|
|
|
980
1116
|
1. A command's `classification` is the **maximum** of:
|
|
@@ -990,14 +1126,13 @@ The `commands` list is recursive — any command entry can contain `inner_comman
|
|
|
990
1126
|
|
|
991
1127
|
## Non-goals
|
|
992
1128
|
|
|
993
|
-
- **Not a sandbox.** This is a classifier, not an enforcer. It does not execute or block anything.
|
|
1129
|
+
- **Not a sandbox.** This is a classifier, not an enforcer. It does not execute or block anything. `match` mode is no exception: it reports which declared command shapes an expression invokes, and the caller decides what to do about it.
|
|
994
1130
|
- **No awk/sed/perl script analysis.** These are classified as a whole command; their embedded programs are opaque. They should simply not be in the READONLY allowlist.
|
|
995
1131
|
- **No variable resolution.** `$DIR`, `$(cmd)` in command position → UNKNOWN. We classify what we can see statically.
|
|
996
1132
|
- **No alias/function resolution.** We classify the literal command name as written.
|
|
997
1133
|
|
|
998
1134
|
## Future extensions
|
|
999
1135
|
|
|
1000
|
-
- `--format=decision` — output only the classification string (for use in hooks)
|
|
1001
1136
|
- `--database=/path` — custom database directory
|
|
1002
1137
|
- `--explain` — verbose output showing the matching steps
|
|
1003
1138
|
- Interactive database builder — run `command --help` and generate a skeleton YAML definition
|
|
@@ -330,20 +330,43 @@ options:
|
|
|
330
330
|
-c: {takes_value: true}
|
|
331
331
|
```
|
|
332
332
|
|
|
333
|
+
### `args_are_expression`
|
|
334
|
+
|
|
335
|
+
Every argument is joined with single spaces and the result is parsed as a complete shell expression. This is what
|
|
336
|
+
`eval` does: it concatenates its arguments and runs the concatenation as shell source, so `eval ls -la` and
|
|
337
|
+
`eval "ls -la"` are the same command. Unlike `flag_value_is_expression`, no flag selects the expression -- every
|
|
338
|
+
token after the binary is part of it, so a command using this mode must not declare options of its own.
|
|
339
|
+
|
|
340
|
+
**eval:** `eval "git push --force"` -- the string `git push --force` is parsed as a full expression, producing one
|
|
341
|
+
inner command.
|
|
342
|
+
|
|
343
|
+
```yaml
|
|
344
|
+
command: eval
|
|
345
|
+
classification: DANGEROUS
|
|
346
|
+
strict: false
|
|
347
|
+
delegates_to:
|
|
348
|
+
mode: args_are_expression
|
|
349
|
+
min_classification: DANGEROUS # eval stays DANGEROUS even when the inner is READONLY
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The `min_classification` floor is not optional here. Command-level delegation erases the wrapper's own base
|
|
353
|
+
classification once it resolves an inner command, so without the floor `eval "ls"` would classify READONLY.
|
|
354
|
+
|
|
333
355
|
### Delegation fields
|
|
334
356
|
|
|
335
357
|
| Field | Type | Applies to | Description |
|
|
336
358
|
|-------|------|-----------|-------------|
|
|
337
|
-
| `mode` | enum | all | One of the
|
|
359
|
+
| `mode` | enum | all | One of the modes above |
|
|
338
360
|
| `separator` | string | `after_separator` | Token that separates wrapper args from inner args |
|
|
339
361
|
| `terminator` | string | `terminated_argv` | Token that ends the inner argv |
|
|
340
362
|
| `flag` | string | `flag_value_is_expression` | Which flag's value to parse as an expression |
|
|
363
|
+
| `skip_leading_positionals` | integer | `rest_are_argv` | Positional tokens belonging to the wrapper itself before the inner command starts (e.g. `timeout DURATION COMMAND...` uses `1`) |
|
|
341
364
|
| `strip_assignments` | boolean | `rest_are_argv` | Strip leading `KEY=VALUE` tokens before inner command |
|
|
342
365
|
| `min_classification` | enum | all | Floor classification for the inner command |
|
|
343
366
|
|
|
344
367
|
### `min_classification`
|
|
345
368
|
|
|
346
|
-
Forces the inner command to be classified at least at the given level. sudo uses this to ensure that even `sudo ls` is at least EXTERNAL_EFFECTS -- because running anything under elevated privileges is not a no-op.
|
|
369
|
+
Forces the inner command to be classified at least at the given level. sudo uses this to ensure that even `sudo ls` is at least EXTERNAL_EFFECTS -- because running anything under elevated privileges is not a no-op. `eval` and `exec` use it the same way, with a `DANGEROUS` floor.
|
|
347
370
|
|
|
348
371
|
## 7. Special Cases
|
|
349
372
|
|
|
@@ -362,9 +385,14 @@ These cannot be modeled as database entries because they are shell builtins with
|
|
|
362
385
|
| Builtin | Classification | Reason |
|
|
363
386
|
|---------|---------------|--------|
|
|
364
387
|
| `cd`, `pushd`, `popd` | READONLY | Directory navigation only |
|
|
365
|
-
| `
|
|
366
|
-
| `source`, `.` | DANGEROUS | Executes an external script |
|
|
367
|
-
|
|
388
|
+
| `[`, `[[`, `test` | READONLY | Condition evaluation, no side effects |
|
|
389
|
+
| `source`, `.` | DANGEROUS | Executes an external script, whose contents are never visible |
|
|
390
|
+
|
|
391
|
+
`eval` and `exec` used to be in this list. They are database entries now -- `commands/eval.yaml`
|
|
392
|
+
delegates via `args_are_expression`, `commands/exec.yaml` via `rest_are_argv`, both with
|
|
393
|
+
`min_classification: DANGEROUS`. Modelling them in YAML exposes the inner command instead of
|
|
394
|
+
hiding it, and the floor is what keeps them DANGEROUS even when the inner command is READONLY.
|
|
395
|
+
Reach for a hardcoded builtin only when the command's argument is genuinely not a command line.
|
|
368
396
|
|
|
369
397
|
### Path-qualified commands
|
|
370
398
|
|
|
@@ -94,7 +94,13 @@
|
|
|
94
94
|
"properties": {
|
|
95
95
|
"mode": {
|
|
96
96
|
"type": "string",
|
|
97
|
-
"enum": [
|
|
97
|
+
"enum": [
|
|
98
|
+
"rest_are_argv",
|
|
99
|
+
"after_separator",
|
|
100
|
+
"terminated_argv",
|
|
101
|
+
"flag_value_is_expression",
|
|
102
|
+
"args_are_expression"
|
|
103
|
+
],
|
|
98
104
|
"description": "Delegation mode"
|
|
99
105
|
},
|
|
100
106
|
"separator": {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/fprochazka/bash-classify/schemas/match-rules.schema.json",
|
|
4
|
+
"title": "bash-classify match rules",
|
|
5
|
+
"description": "Command shapes for `bash-classify match --rules`. Within one rule every given condition must hold; rules are independent of each other.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["rules"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"rules": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"minItems": 1,
|
|
13
|
+
"items": { "$ref": "#/$defs/Rule" },
|
|
14
|
+
"description": "The command shapes to look for"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"$defs": {
|
|
18
|
+
"CommandPath": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"minItems": 1,
|
|
21
|
+
"items": { "type": "string", "minLength": 1 },
|
|
22
|
+
"description": "A resolved command path, binary first: [glab, mr, note]"
|
|
23
|
+
},
|
|
24
|
+
"Rule": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"additionalProperties": false,
|
|
27
|
+
"required": ["name", "command"],
|
|
28
|
+
"properties": {
|
|
29
|
+
"name": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"minLength": 1,
|
|
32
|
+
"description": "Rule name, unique within the file; reported back on every match"
|
|
33
|
+
},
|
|
34
|
+
"command": {
|
|
35
|
+
"$ref": "#/$defs/CommandPath",
|
|
36
|
+
"description": "Prefix match against the resolved command path, so [glab, mr, note] also matches `glab --repo x mr note`"
|
|
37
|
+
},
|
|
38
|
+
"except": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"items": { "$ref": "#/$defs/CommandPath" },
|
|
41
|
+
"description": "Command paths excluded from the match, also by prefix"
|
|
42
|
+
},
|
|
43
|
+
"any_option": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"minItems": 1,
|
|
46
|
+
"items": { "type": "string", "pattern": "^-" },
|
|
47
|
+
"description": "At least one of these options must be present, as typed; a declared cluster -wc counts as -w and -c, and --key=value counts as --key"
|
|
48
|
+
},
|
|
49
|
+
"any_arg_matches": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"minLength": 1,
|
|
52
|
+
"description": "Python re.search pattern applied to every argument token: argv[1:] minus the resolved subcommand words"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
"""bash-classify: Classify bash expressions by their side-effect risk level."""
|
|
2
2
|
|
|
3
|
-
from bash_classify.classifier import classify_expression
|
|
3
|
+
from bash_classify.classifier import classify_expression, iter_invocations
|
|
4
4
|
from bash_classify.database import CommandDatabase, load_database
|
|
5
5
|
from bash_classify.models import Classification, CommandResult, ExpressionResult, Risk
|
|
6
|
+
from bash_classify.rules import Match, MatchResult, Rule, RulesError, load_rules, match_expression
|
|
6
7
|
|
|
7
8
|
__all__ = [
|
|
8
9
|
"classify_expression",
|
|
10
|
+
"iter_invocations",
|
|
9
11
|
"load_database",
|
|
10
12
|
"CommandDatabase",
|
|
11
13
|
"Classification",
|
|
12
14
|
"Risk",
|
|
13
15
|
"ExpressionResult",
|
|
14
16
|
"CommandResult",
|
|
17
|
+
"Rule",
|
|
18
|
+
"RulesError",
|
|
19
|
+
"Match",
|
|
20
|
+
"MatchResult",
|
|
21
|
+
"load_rules",
|
|
22
|
+
"match_expression",
|
|
15
23
|
]
|