argocd-source-lint 1.0.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.
- argocd_source_lint-1.0.0/.argocd-lint.example.yaml +31 -0
- argocd_source_lint-1.0.0/.github/workflows/test.yml +49 -0
- argocd_source_lint-1.0.0/.gitignore +11 -0
- argocd_source_lint-1.0.0/.pre-commit-hooks.yaml +7 -0
- argocd_source_lint-1.0.0/CHANGELOG.md +508 -0
- argocd_source_lint-1.0.0/CONTRIBUTING.md +101 -0
- argocd_source_lint-1.0.0/DESIGN.md +1133 -0
- argocd_source_lint-1.0.0/LICENSE +21 -0
- argocd_source_lint-1.0.0/PKG-INFO +306 -0
- argocd_source_lint-1.0.0/README.md +279 -0
- argocd_source_lint-1.0.0/SECURITY.md +58 -0
- argocd_source_lint-1.0.0/action.yml +61 -0
- argocd_source_lint-1.0.0/pyproject.toml +64 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/__init__.py +10 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/app_projects.py +53 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/applicationset.py +451 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/baseline.py +94 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/cli.py +221 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/coverage.py +284 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/fsutil.py +199 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/git_context.py +301 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/globs.py +70 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/loader.py +153 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/models.py +125 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/policy.py +60 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/reporters/__init__.py +0 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/reporters/fingerprint.py +19 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/reporters/gitlab_codequality.py +36 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/reporters/json_report.py +18 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/reporters/junit.py +67 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/reporters/sarif.py +125 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/reporters/table.py +53 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/__init__.py +0 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/base.py +37 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/broken_values_ref.py +199 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/double_coverage.py +64 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/duplicate_application_name.py +67 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/hpa_selfheal_conflict.py +153 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/known-operators.yaml +89 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/malformed_ignore_diff_jq_expression.py +62 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/malformed_ignore_diff_pointer.py +55 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/malformed_sync_wave.py +79 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/missing_ignore_diff.py +181 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/orphan_source.py +105 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/phantom_target.py +73 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/project_scope.py +204 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/revision_mismatch.py +61 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/sync_validation_disabled.py +53 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/unknown_resource_hook.py +116 -0
- argocd_source_lint-1.0.0/src/argocd_source_lint/rules/unknown_sync_option.py +151 -0
- argocd_source_lint-1.0.0/templates/lint.yml +38 -0
- argocd_source_lint-1.0.0/tests/conftest.py +92 -0
- argocd_source_lint-1.0.0/tests/fixtures/bad_broken_values_ref/bootstrap/argocd-apps/broken-ref-file-app.yaml +15 -0
- argocd_source_lint-1.0.0/tests/fixtures/bad_broken_values_ref/bootstrap/argocd-apps/broken-ref-name-app.yaml +12 -0
- argocd_source_lint-1.0.0/tests/fixtures/bad_missing_ignore_diff/bootstrap/argocd-apps/pg-app.yaml +13 -0
- argocd_source_lint-1.0.0/tests/fixtures/bad_missing_ignore_diff/infrastructure/pg/cluster.yaml +7 -0
- argocd_source_lint-1.0.0/tests/fixtures/bad_orphan_source/.argocd-lint.yaml +2 -0
- argocd_source_lint-1.0.0/tests/fixtures/bad_orphan_source/bootstrap/argocd-apps/demo-app.yaml +13 -0
- argocd_source_lint-1.0.0/tests/fixtures/bad_orphan_source/manifests/demo-app/deployment.yaml +6 -0
- argocd_source_lint-1.0.0/tests/fixtures/bad_orphan_source/manifests/ignored-orphan/configmap.yaml +7 -0
- argocd_source_lint-1.0.0/tests/fixtures/bad_orphan_source/manifests/orphaned/configmap.yaml +6 -0
- argocd_source_lint-1.0.0/tests/fixtures/bad_phantom_target/bootstrap/argocd-apps/demo-app.yaml +10 -0
- argocd_source_lint-1.0.0/tests/fixtures/good_repo/.argocd-lint.yaml +2 -0
- argocd_source_lint-1.0.0/tests/fixtures/good_repo/bootstrap/argocd-apps/demo-app.yaml +13 -0
- argocd_source_lint-1.0.0/tests/fixtures/good_repo/manifests/demo-app/deployment.yaml +6 -0
- argocd_source_lint-1.0.0/tests/fixtures/good_repo/manifests/demo-app/service.yaml +7 -0
- argocd_source_lint-1.0.0/tests/test_applicationset.py +976 -0
- argocd_source_lint-1.0.0/tests/test_baseline.py +89 -0
- argocd_source_lint-1.0.0/tests/test_cli.py +261 -0
- argocd_source_lint-1.0.0/tests/test_coverage.py +243 -0
- argocd_source_lint-1.0.0/tests/test_fsutil.py +144 -0
- argocd_source_lint-1.0.0/tests/test_git_context.py +191 -0
- argocd_source_lint-1.0.0/tests/test_globs.py +72 -0
- argocd_source_lint-1.0.0/tests/test_loader.py +340 -0
- argocd_source_lint-1.0.0/tests/test_policy.py +51 -0
- argocd_source_lint-1.0.0/tests/test_reporters.py +265 -0
- argocd_source_lint-1.0.0/tests/test_rules_broken_values_ref.py +309 -0
- argocd_source_lint-1.0.0/tests/test_rules_double_coverage.py +213 -0
- argocd_source_lint-1.0.0/tests/test_rules_duplicate_application_name.py +207 -0
- argocd_source_lint-1.0.0/tests/test_rules_hpa_selfheal_conflict.py +246 -0
- argocd_source_lint-1.0.0/tests/test_rules_malformed_ignore_diff_jq_expression.py +141 -0
- argocd_source_lint-1.0.0/tests/test_rules_malformed_ignore_diff_pointer.py +137 -0
- argocd_source_lint-1.0.0/tests/test_rules_malformed_sync_wave.py +134 -0
- argocd_source_lint-1.0.0/tests/test_rules_missing_ignore_diff.py +648 -0
- argocd_source_lint-1.0.0/tests/test_rules_orphan_source.py +416 -0
- argocd_source_lint-1.0.0/tests/test_rules_phantom_target.py +179 -0
- argocd_source_lint-1.0.0/tests/test_rules_project_scope.py +423 -0
- argocd_source_lint-1.0.0/tests/test_rules_revision_mismatch.py +184 -0
- argocd_source_lint-1.0.0/tests/test_rules_sync_validation_disabled.py +94 -0
- argocd_source_lint-1.0.0/tests/test_rules_unknown_resource_hook.py +136 -0
- argocd_source_lint-1.0.0/tests/test_rules_unknown_sync_option.py +217 -0
- argocd_source_lint-1.0.0/tests/test_version.py +18 -0
- argocd_source_lint-1.0.0/uv.lock +340 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Copy to .argocd-lint.yaml at the repo root and adapt.
|
|
2
|
+
# Any omitted key keeps its default value.
|
|
3
|
+
|
|
4
|
+
scan_roots:
|
|
5
|
+
- manifests/
|
|
6
|
+
- infra/
|
|
7
|
+
|
|
8
|
+
rules:
|
|
9
|
+
orphan-source: error
|
|
10
|
+
broken-values-ref: error
|
|
11
|
+
missing-ignore-diff: warning
|
|
12
|
+
phantom-target: error
|
|
13
|
+
unresolvable-generator: info
|
|
14
|
+
double-coverage: error
|
|
15
|
+
revision-mismatch: info
|
|
16
|
+
project-scope-violation: error
|
|
17
|
+
hpa-selfheal-conflict: warning
|
|
18
|
+
sync-validation-disabled: info
|
|
19
|
+
duplicate-application-name: error
|
|
20
|
+
malformed-ignore-diff-pointer: warning
|
|
21
|
+
malformed-ignore-diff-jq-expression: warning
|
|
22
|
+
unknown-sync-option: warning
|
|
23
|
+
unknown-resource-hook: warning
|
|
24
|
+
malformed-sync-wave: warning
|
|
25
|
+
|
|
26
|
+
# Default: true (fail-closed). Only set to false knowingly if the team
|
|
27
|
+
# accepts running on an incomplete shallow clone in CI.
|
|
28
|
+
unverifiable_blocks_ci: true
|
|
29
|
+
|
|
30
|
+
exclude_paths:
|
|
31
|
+
- manifests/legacy/**
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- name: Install uv
|
|
15
|
+
uses: astral-sh/setup-uv@v4
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.11"
|
|
18
|
+
|
|
19
|
+
- name: Install dependencies
|
|
20
|
+
run: uv sync --extra dev
|
|
21
|
+
|
|
22
|
+
- name: Ruff check
|
|
23
|
+
run: uv run ruff check .
|
|
24
|
+
|
|
25
|
+
- name: Ruff format check
|
|
26
|
+
run: uv run ruff format --check .
|
|
27
|
+
|
|
28
|
+
pytest:
|
|
29
|
+
strategy:
|
|
30
|
+
fail-fast: false
|
|
31
|
+
matrix:
|
|
32
|
+
os: [ubuntu-latest, windows-latest]
|
|
33
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
34
|
+
|
|
35
|
+
runs-on: ${{ matrix.os }}
|
|
36
|
+
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v4
|
|
39
|
+
|
|
40
|
+
- name: Install uv
|
|
41
|
+
uses: astral-sh/setup-uv@v4
|
|
42
|
+
with:
|
|
43
|
+
python-version: ${{ matrix.python-version }}
|
|
44
|
+
|
|
45
|
+
- name: Install dependencies
|
|
46
|
+
run: uv sync --extra dev
|
|
47
|
+
|
|
48
|
+
- name: Run tests
|
|
49
|
+
run: uv run pytest -q
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file, in
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
|
|
5
|
+
|
|
6
|
+
## [1.0.0] - 2026-09-22
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- Version bumped to `1.0.0`, marking the public v1 launch (real PyPI +
|
|
11
|
+
GitHub Marketplace). No functional change from `0.1.36` — every rule,
|
|
12
|
+
reporter, and the two security-audit rounds behind it are unchanged;
|
|
13
|
+
this is the version/metadata declaration that milestone was gated on.
|
|
14
|
+
PyPI classifier updated from `Development Status :: 3 - Alpha` to
|
|
15
|
+
`5 - Production/Stable`.
|
|
16
|
+
|
|
17
|
+
## [0.1.36] - 2026-09-22
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
|
|
21
|
+
A second, more thorough audit pass (explicitly requested: "tu en vois
|
|
22
|
+
d'autres ? ... je veux en être sûr"), checked systematically by
|
|
23
|
+
category rather than opportunistically. Three more real, confirmed
|
|
24
|
+
issues:
|
|
25
|
+
|
|
26
|
+
- `materialize_revision` crashed the whole CLI on a hostile tar
|
|
27
|
+
stream instead of returning `None`. `filter="data"` (PEP 706)
|
|
28
|
+
correctly rejects a `../`-style tar entry — confirmed for real, it
|
|
29
|
+
raises `OutsideDestinationError` rather than extracting outside the
|
|
30
|
+
target directory — but that rejection is itself an exception, and
|
|
31
|
+
nothing caught it. Now wrapped in `try: ... except tarfile.TarError`,
|
|
32
|
+
same effect as the existing "subprocess failed" branch beside it.
|
|
33
|
+
- The table reporter interpreted repo-controlled content (an
|
|
34
|
+
Application's own name, a filename, a rule's message) as `rich`
|
|
35
|
+
markup. Confirmed for real: a crafted `metadata.name` containing
|
|
36
|
+
`[link=...]` rendered as an actual clickable hyperlink, and a
|
|
37
|
+
`[bold red on white]`-style tag actually re-styled the row — a
|
|
38
|
+
crafted repo could restyle or spoof this tool's own terminal output.
|
|
39
|
+
Every field except the severity cell (built from this tool's own
|
|
40
|
+
closed enum, never repo content) is now escaped with
|
|
41
|
+
`rich.markup.escape()`.
|
|
42
|
+
- The ApplicationSet `matrix` generator's cartesian product had no
|
|
43
|
+
size cap — only the "max 2 children" structural cap existed, nothing
|
|
44
|
+
about how large those two children's own param lists could be.
|
|
45
|
+
Confirmed for real: two `list` generators of 5,000 small elements
|
|
46
|
+
each (individually well under the alias-bomb node budget, since that
|
|
47
|
+
budget catches a densely aliased document, not a large flat one)
|
|
48
|
+
produced 25,000,000 combinations in ~7s for the combine step alone,
|
|
49
|
+
before a single generated Application is even built. Capped at
|
|
50
|
+
10,000 combinations, checked before the cartesian product is built.
|
|
51
|
+
|
|
52
|
+
Checked and confirmed not vulnerable: `ruamel.yaml`'s loaders don't
|
|
53
|
+
execute Python object construction from YAML tags; the JUnit/SARIF/
|
|
54
|
+
JSON/GitLab reporters all escape properly (`ElementTree`/`json.dumps`,
|
|
55
|
+
never manual string concatenation); `pip-audit` found nothing against
|
|
56
|
+
the exact resolved dependency versions.
|
|
57
|
+
|
|
58
|
+
Documented as a limitation, not a code fix: `.argocd-lint.yaml` and
|
|
59
|
+
`.argocd-lint-baseline.yaml` are repo content themselves, so an
|
|
60
|
+
untrusted fork's PR can edit either in the same PR that introduces
|
|
61
|
+
what it would otherwise flag — protect both with `CODEOWNERS`/branch
|
|
62
|
+
protection if running against untrusted forks.
|
|
63
|
+
|
|
64
|
+
## [0.1.35] - 2026-09-22
|
|
65
|
+
|
|
66
|
+
### Security
|
|
67
|
+
|
|
68
|
+
- `globs.py`'s `match_glob` (shared by the ApplicationSet `git`
|
|
69
|
+
generator's `directories`/`files` `path:` and
|
|
70
|
+
`project-scope-violation`'s `sourceRepos`/`destinations`) translated
|
|
71
|
+
a glob pattern into a backtracking regex. Confirmed for real: a
|
|
72
|
+
pattern shaped like `*a*a*a...*a!` (~40 repetitions) against a
|
|
73
|
+
non-matching candidate of `a`s hung indefinitely — a classic ReDoS,
|
|
74
|
+
and both the pattern and the candidate are repo-controlled here.
|
|
75
|
+
`match_glob` now tokenizes the pattern once and runs a
|
|
76
|
+
dynamic-programming scan over the candidate instead —
|
|
77
|
+
`O(len(pattern) x len(candidate))` by construction, regardless of how
|
|
78
|
+
many wildcards the pattern has, rather than relying on an input-size
|
|
79
|
+
limit that would still be exponential below the limit.
|
|
80
|
+
|
|
81
|
+
## [0.1.34] - 2026-09-22
|
|
82
|
+
|
|
83
|
+
### Security
|
|
84
|
+
|
|
85
|
+
- A source's own `path` (e.g. `path: ../outside-secret`) was joined
|
|
86
|
+
onto the repo root with no check that the result stayed inside it.
|
|
87
|
+
Confirmed for real: this crashed `covered_files_for_application`
|
|
88
|
+
(`orphan-source`, `double-coverage`) and made
|
|
89
|
+
`covered_documents_for_application` (`missing-ignore-diff`,
|
|
90
|
+
`hpa-selfheal-conflict`, `unknown-sync-option`,
|
|
91
|
+
`unknown-resource-hook`, `malformed-sync-wave`) silently read and
|
|
92
|
+
surface a file's full content from anywhere reachable via `../` from
|
|
93
|
+
the repo root — a straight local file disclosure, not just a crash.
|
|
94
|
+
- A second, independent instance of the same bug: a Kustomize overlay's
|
|
95
|
+
own `resources`/`bases`/`components`/`crds`/`patches*`/generator
|
|
96
|
+
entries were resolved the same unguarded way. This one comes from
|
|
97
|
+
*tracked YAML content*, not ArgoCD's own schema, so it doesn't even
|
|
98
|
+
need a crafted `Application` — any commit touching a
|
|
99
|
+
`kustomization.yaml` can reach it, and the recursion means a nested
|
|
100
|
+
overlay chain could walk arbitrarily far outside the repo.
|
|
101
|
+
- Both fixed by threading the repo/snapshot root through the whole
|
|
102
|
+
resolution chain and checking every resolved path against it
|
|
103
|
+
(`Path.is_relative_to`) before it's ever stat'd, walked, or read —
|
|
104
|
+
never filtered out of the result afterward, which would still touch
|
|
105
|
+
arbitrary filesystem locations along the way.
|
|
106
|
+
|
|
107
|
+
## [0.1.33] - 2026-09-22
|
|
108
|
+
|
|
109
|
+
### Security
|
|
110
|
+
|
|
111
|
+
- A `targetRevision` (or an ApplicationSet `git` generator's own
|
|
112
|
+
`revision`) is repo-controlled YAML, but was passed straight through
|
|
113
|
+
as a positional argument to `git`. A value like
|
|
114
|
+
`--remote=https://<host>/x` is read by `git` as a *flag*, not a
|
|
115
|
+
revision — confirmed for real: `git archive` with that value spends
|
|
116
|
+
the full TCP connect timeout actually reaching out to `<host>`
|
|
117
|
+
instead of failing to parse, a live SSRF primitive from inside
|
|
118
|
+
whatever CI job runs this tool, breaking the "no network access, no
|
|
119
|
+
credentials" guarantee the tool is built on. Every function in
|
|
120
|
+
`git_context.py` that shells out with a `revision`
|
|
121
|
+
(`is_revision_resolvable`, `_resolve_commit`, `materialize_revision`,
|
|
122
|
+
`list_tree_paths`) now rejects a `-`-prefixed value before it ever
|
|
123
|
+
reaches a `git` subprocess — a real revision never starts with `-`
|
|
124
|
+
in the first place, so no legitimate value is affected. A rejected
|
|
125
|
+
revision is treated exactly like any other unresolvable one, same
|
|
126
|
+
`unverifiable` handling already in place.
|
|
127
|
+
|
|
128
|
+
## [0.1.32] - 2026-09-22
|
|
129
|
+
|
|
130
|
+
### Added
|
|
131
|
+
|
|
132
|
+
- `malformed-ignore-diff-jq-expression` rule: an `ignoreDifferences`
|
|
133
|
+
`jqPathExpressions` entry that doesn't start with `.` — every real
|
|
134
|
+
example in ArgoCD's own docs does, the same convention `jsonPointers`
|
|
135
|
+
has for `/`. Usually a `jsonPointers`-style path pasted into the
|
|
136
|
+
wrong sibling field. Confirmed against ArgoCD's own source
|
|
137
|
+
(`NewIgnoreNormalizer`): a parse failure here voids the *entire*
|
|
138
|
+
`ignoreDifferences` list for that Application, not just this entry —
|
|
139
|
+
a wider blast radius than a bad `jsonPointers` entry, and matching
|
|
140
|
+
real reports of `jqPathExpressions` that "apply without errors but
|
|
141
|
+
don't actually do anything." Deliberately as narrow as
|
|
142
|
+
`malformed-ignore-diff-pointer`: no `jq` engine involved, no attempt
|
|
143
|
+
to catch ArgoCD's own version-dependent evaluation quirks.
|
|
144
|
+
|
|
145
|
+
### Fixed
|
|
146
|
+
|
|
147
|
+
- `policy.DEFAULT_RULE_SEVERITIES` and `reporters/sarif.py`'s
|
|
148
|
+
`_RULES_METADATA` are two more manually maintained rule-ID lists,
|
|
149
|
+
same shape as the `cli.RULES` gap closed in `0.1.31` — a rule
|
|
150
|
+
missing from either wouldn't crash, just silently lose its
|
|
151
|
+
configurable severity or its SARIF `fullDescription`/`helpUri`. Two
|
|
152
|
+
new tests assert both stay in sync with the real rule set.
|
|
153
|
+
|
|
154
|
+
## [0.1.31] - 2026-09-22
|
|
155
|
+
|
|
156
|
+
### Fixed
|
|
157
|
+
|
|
158
|
+
- Five rules (`missing-ignore-diff`, `hpa-selfheal-conflict`,
|
|
159
|
+
`unknown-sync-option`, `unknown-resource-hook`, `malformed-sync-wave`)
|
|
160
|
+
and two more (`orphan-source`, `double-coverage`) each independently
|
|
161
|
+
re-walked and re-parsed the same Application's covered source
|
|
162
|
+
directory — confirmed for real: 40 Applications x 15 covered files
|
|
163
|
+
each, across just 3 of those rules, cost ~4.9s of pure re-walking and
|
|
164
|
+
re-parsing. `fsutil.load_yaml_documents` now caches by resolved file
|
|
165
|
+
path and `coverage.covered_files_for_source` caches by
|
|
166
|
+
`(source_dir, directory_recurse, directory_include,
|
|
167
|
+
directory_exclude)` — both reset via `clear_caches`, same story as
|
|
168
|
+
every other cache here.
|
|
169
|
+
- `phantom-target` ran one `git ls-tree` subprocess per Application to
|
|
170
|
+
check its `path` exists at `targetRevision`, even though most
|
|
171
|
+
Applications in a real repo share the same revision — confirmed for
|
|
172
|
+
real: 40 Applications with distinct paths cost ~2.3s of subprocess
|
|
173
|
+
spawns. `git_context.tree_paths_at_revision` now lists the whole tree
|
|
174
|
+
once per `(repo_root, revision)`, and `path_has_tracked_files`
|
|
175
|
+
answers the per-path question against that in-memory listing instead.
|
|
176
|
+
`broken-values-ref`'s own equivalent local cache is now backed by the
|
|
177
|
+
same shared one.
|
|
178
|
+
- A `Rule` subclass that exists under `rules/` but is never added to
|
|
179
|
+
`cli.RULES` stayed invisible to the whole test suite: its own unit
|
|
180
|
+
test instantiates and calls it directly, so it could pass while the
|
|
181
|
+
real CLI never ran it at all. Confirmed for real with a throwaway
|
|
182
|
+
rule module. `test_every_rule_class_is_registered_in_cli_rules`
|
|
183
|
+
force-imports every module under `rules/` and asserts none are
|
|
184
|
+
missing from `RULES`.
|
|
185
|
+
|
|
186
|
+
### Changed
|
|
187
|
+
|
|
188
|
+
- `orphan-source` imported `broken_values_ref.py`'s `_parse_ref_entry`
|
|
189
|
+
across module boundaries despite its underscore signaling "private" —
|
|
190
|
+
it already was shared, just not admitting it. Renamed to
|
|
191
|
+
`parse_ref_entry`, and the `{source.ref: source for source in
|
|
192
|
+
app.sources if source.ref}` comprehension duplicated verbatim in both
|
|
193
|
+
files is now the shared `ref_sources_by_name`. No behavior change.
|
|
194
|
+
|
|
195
|
+
## [0.1.30] - 2026-09-21
|
|
196
|
+
|
|
197
|
+
### Changed
|
|
198
|
+
|
|
199
|
+
- `applicationset.py`'s four generator resolvers
|
|
200
|
+
(`_resolve_generator`/`_resolve_git`/`_resolve_matrix`/
|
|
201
|
+
`_resolve_merge`) each repeated the same five parameters
|
|
202
|
+
(`repo_root`, `local_origin`, `appset_name`, `source_file`,
|
|
203
|
+
`severity`) alongside their own generator dict. Replaced with a
|
|
204
|
+
single `GeneratorContext`. Internal only, no behavior change, no
|
|
205
|
+
call site outside this file.
|
|
206
|
+
|
|
207
|
+
## [0.1.29] - 2026-09-21
|
|
208
|
+
|
|
209
|
+
### Fixed
|
|
210
|
+
|
|
211
|
+
- `RawManifestDiscovery`, the ApplicationSet walker and
|
|
212
|
+
`discover_app_projects` each independently walked the entire repo
|
|
213
|
+
and parsed every YAML file, each looking for a different `kind` —
|
|
214
|
+
three full passes over the same files. Confirmed for real: 2,000
|
|
215
|
+
plain manifests (none of them an Application/ApplicationSet/
|
|
216
|
+
AppProject) cost ~5s total across the three passes.
|
|
217
|
+
`fsutil.discover_documents` now does that walk once and caches it
|
|
218
|
+
(same scoping as `_resolve_commit`'s cache, reset via
|
|
219
|
+
`clear_caches`); the three callers just filter the shared result by
|
|
220
|
+
`kind`. Every existing call site's own signature is unchanged.
|
|
221
|
+
|
|
222
|
+
## [0.1.28] - 2026-09-21
|
|
223
|
+
|
|
224
|
+
### Fixed
|
|
225
|
+
|
|
226
|
+
- 50 Applications sharing `targetRevision: HEAD` cost 702 identical
|
|
227
|
+
`git rev-parse` calls in a single run — confirmed for real, not
|
|
228
|
+
estimated: ~58 seconds on Windows, where subprocess spawn dominates
|
|
229
|
+
the cost. Every local-source rule independently re-resolved the same
|
|
230
|
+
revision against the same checkout with no caching at all.
|
|
231
|
+
`git_context._resolve_commit` now caches by `(repo_root, revision)`
|
|
232
|
+
for the life of one CLI invocation, collapsing the same run to 1
|
|
233
|
+
call and ~5 seconds. A caching change must never alter results, only
|
|
234
|
+
speed — verified: identical findings before and after, and a
|
|
235
|
+
dedicated test confirms a repo mutated between two invocations still
|
|
236
|
+
gets a fresh answer (the one scenario a naive process-lifetime cache
|
|
237
|
+
could get wrong).
|
|
238
|
+
|
|
239
|
+
## [0.1.27] - 2026-09-21
|
|
240
|
+
|
|
241
|
+
### Fixed
|
|
242
|
+
|
|
243
|
+
- A directory symlink/junction pointing back at one of its own
|
|
244
|
+
ancestors made file discovery run forever — confirmed for real with
|
|
245
|
+
a Windows junction (a few KB on disk). Neither `Path.rglob` nor even
|
|
246
|
+
`os.walk(directory, followlinks=False)` protect against it: a
|
|
247
|
+
junction isn't reported as a symlink, so that guard never triggers.
|
|
248
|
+
`fsutil.walk_tree` replaces three independent, unguarded `rglob`
|
|
249
|
+
call sites (`iter_yaml_files`, and the ApplicationSet `git`
|
|
250
|
+
generator's own directory/file discovery) with one shared,
|
|
251
|
+
cycle-safe walk — each directory resolved once and never
|
|
252
|
+
re-descended into, the same identity-tracking already used for a
|
|
253
|
+
Kustomize `resources:` cycle. As a side effect, a file reachable
|
|
254
|
+
through two different non-cyclic paths to the same physical
|
|
255
|
+
directory is now found once per traversal instead of double-counted.
|
|
256
|
+
|
|
257
|
+
## [0.1.26] - 2026-09-21
|
|
258
|
+
|
|
259
|
+
### Fixed
|
|
260
|
+
|
|
261
|
+
- A YAML "billion laughs" alias bomb (a few hundred bytes, nested
|
|
262
|
+
anchors each referencing the previous one twice) hung the tool
|
|
263
|
+
indefinitely — confirmed for real against both an ApplicationSet
|
|
264
|
+
`git` generator's `files:` params file and a manifest with a bombed
|
|
265
|
+
`apiVersion` field. `ruamel.yaml`'s own loader is safe (an alias
|
|
266
|
+
resolves to the same object, not a copy); the hang came from code
|
|
267
|
+
downstream blindly stringifying the result. `fsutil.load_yaml_documents`
|
|
268
|
+
now rejects a document whose fully-expanded size would be
|
|
269
|
+
unreasonable (`is_within_budget`, computed via memoized recursion so
|
|
270
|
+
the check itself stays cheap), the same way it already drops a file
|
|
271
|
+
that fails to parse — closing this at the one place virtually every
|
|
272
|
+
document is read through, rather than patching each of the five
|
|
273
|
+
near-identical unguarded `str()` call sites this audit found.
|
|
274
|
+
|
|
275
|
+
## [0.1.25] - 2026-09-21
|
|
276
|
+
|
|
277
|
+
### Fixed
|
|
278
|
+
|
|
279
|
+
- A missing `git` binary silently degraded every result into false
|
|
280
|
+
`orphan-source`/`double-coverage` positives instead of a clear error:
|
|
281
|
+
`get_origin_url` was the only call site that handled it, by returning
|
|
282
|
+
`None` — the exact same value it returns for "no `origin` remote
|
|
283
|
+
configured," which every rule already (correctly, for that case)
|
|
284
|
+
treats as "nothing here is local." Reproduced against
|
|
285
|
+
`python:3.11-slim`, the base image `templates/lint.yml` itself
|
|
286
|
+
recommends, which doesn't ship `git`. Fixed on both ends: the CLI now
|
|
287
|
+
checks `git` is on PATH at startup and exits with a clear error
|
|
288
|
+
instead of degrading, and `templates/lint.yml` installs `git` in
|
|
289
|
+
`before_script`.
|
|
290
|
+
|
|
291
|
+
## [0.1.24] - 2026-09-21
|
|
292
|
+
|
|
293
|
+
### Fixed
|
|
294
|
+
|
|
295
|
+
- The ApplicationSet `git` generator's own `revision` field
|
|
296
|
+
(`directories`/`files`, independent of any generated Application's
|
|
297
|
+
`targetRevision`) was never read at all — directory/file discovery
|
|
298
|
+
always used the checked-out working tree. A `revision` pinned away
|
|
299
|
+
from HEAD would silently generate Applications from today's directory
|
|
300
|
+
structure instead of the pinned one's, with no finding raised at all
|
|
301
|
+
— worse than the other `unresolvable-generator` cases, which are at
|
|
302
|
+
least flagged. Now resolved against a snapshot of that revision, the
|
|
303
|
+
same mechanism already used for a source's own `targetRevision`
|
|
304
|
+
(`git_context.materialize_revision`, moved there from `coverage.py`
|
|
305
|
+
so both call sites share it). An unresolvable revision is reported
|
|
306
|
+
`unverifiable`, same as `phantom-target`/`broken-values-ref`'s
|
|
307
|
+
shallow-clone case.
|
|
308
|
+
|
|
309
|
+
## [0.1.23] - 2026-09-21
|
|
310
|
+
|
|
311
|
+
### Fixed
|
|
312
|
+
|
|
313
|
+
- SARIF's `$schema` pointed at the spec repo's mutable `master` branch
|
|
314
|
+
instead of GitHub's own recommended
|
|
315
|
+
`https://json.schemastore.org/sarif-2.1.0.json`. Results had no
|
|
316
|
+
`partialFingerprints`, which GitHub's docs call essential for
|
|
317
|
+
matching the same finding across runs — without it, an unrelated
|
|
318
|
+
change elsewhere can make every finding look like a new alert.
|
|
319
|
+
- JUnit `<testcase name=...>` was the finding's raw message, and two
|
|
320
|
+
different findings (different file/Application) can share that
|
|
321
|
+
message verbatim — GitLab's JUnit parser silently drops every
|
|
322
|
+
testcase after the first with a duplicate name. A `(#N)` suffix on an
|
|
323
|
+
exact repeat keeps every finding visible.
|
|
324
|
+
- README's "pending real PyPI" installation caveat wrongly included the
|
|
325
|
+
pre-commit hook (it builds from this Git repo via `pip install .`,
|
|
326
|
+
never from PyPI, so it already works today) and omitted the GitLab CI
|
|
327
|
+
component (which does `pip install` from the real PyPI index, and is
|
|
328
|
+
genuinely affected).
|
|
329
|
+
- README's GitHub Actions example was missing `actions/checkout` —
|
|
330
|
+
without it there's nothing for the action to analyze, and it silently
|
|
331
|
+
"succeeds" with zero findings instead of erroring — and the
|
|
332
|
+
`permissions: security-events: write` (`actions`/`contents: read` on
|
|
333
|
+
private repos) the composite action's `upload-sarif` step needs, which
|
|
334
|
+
GitHub never grants automatically to a composite action.
|
|
335
|
+
- README's GitLab CI section overstated the prerequisite: using
|
|
336
|
+
`include: component:` never requires publishing to the CI/CD
|
|
337
|
+
Catalog, only a `README.md` + `templates/` in a project on the same
|
|
338
|
+
GitLab instance, referenced by any ref (tag, branch or commit SHA) —
|
|
339
|
+
confirmed against GitLab's own component docs. Publishing to the
|
|
340
|
+
Catalog is a separate, optional step for public discoverability.
|
|
341
|
+
|
|
342
|
+
### Added
|
|
343
|
+
|
|
344
|
+
- Rules now carry `fullDescription`, `helpUri` and
|
|
345
|
+
`defaultConfiguration.level` in the SARIF output (recommended fields
|
|
346
|
+
GitHub's UI uses for filtering/detail pages).
|
|
347
|
+
|
|
348
|
+
## [0.1.22] - 2026-09-21
|
|
349
|
+
|
|
350
|
+
### Fixed
|
|
351
|
+
|
|
352
|
+
- `directory.include`/`exclude` and `.argocd-lint.yaml`'s `exclude_paths`
|
|
353
|
+
now match case-sensitively. `fnmatch.fnmatch` case-folds on Windows,
|
|
354
|
+
which could silently diverge from ArgoCD's own case-sensitive Go
|
|
355
|
+
`filepath.Match` and from a case-sensitive CI runner.
|
|
356
|
+
|
|
357
|
+
## [0.1.21] - 2026-09-21
|
|
358
|
+
|
|
359
|
+
### Fixed
|
|
360
|
+
|
|
361
|
+
- `broken-values-ref` now also checks plain (non-`$ref`) Helm
|
|
362
|
+
`valueFiles` entries against the repo, not just cross-source `$ref`
|
|
363
|
+
entries.
|
|
364
|
+
|
|
365
|
+
## [0.1.20] - 2026-09-21
|
|
366
|
+
|
|
367
|
+
### Added
|
|
368
|
+
|
|
369
|
+
- `malformed-sync-wave` rule: a non-integer
|
|
370
|
+
`argocd.argoproj.io/sync-wave` annotation value, which ArgoCD silently
|
|
371
|
+
falls back to wave 0 for instead of erroring.
|
|
372
|
+
|
|
373
|
+
## [0.1.19] - 2026-09-21
|
|
374
|
+
|
|
375
|
+
### Added
|
|
376
|
+
|
|
377
|
+
- Keycloak Operator and External Secrets Operator signatures to the
|
|
378
|
+
built-in `known_operators` pack.
|
|
379
|
+
|
|
380
|
+
### Changed
|
|
381
|
+
|
|
382
|
+
- Clarified the GitLab CI `<namespace>` placeholder (renamed to
|
|
383
|
+
`<your-gitlab-group>`, documented the GitLab CI/CD Catalog publishing
|
|
384
|
+
prerequisite).
|
|
385
|
+
|
|
386
|
+
## [0.1.18] - 2026-09-21
|
|
387
|
+
|
|
388
|
+
### Added
|
|
389
|
+
|
|
390
|
+
- `unknown-resource-hook` rule: an unrecognized
|
|
391
|
+
`argocd.argoproj.io/hook`/`hook-delete-policy` annotation value.
|
|
392
|
+
- Zalando Postgres Operator signature to `known_operators`.
|
|
393
|
+
|
|
394
|
+
### Changed
|
|
395
|
+
|
|
396
|
+
- `unknown-sync-option` now also checks the per-resource
|
|
397
|
+
`argocd.argoproj.io/sync-options` annotation, not just the
|
|
398
|
+
Application-level `syncOptions`.
|
|
399
|
+
|
|
400
|
+
## [0.1.17] - 2026-09-21
|
|
401
|
+
|
|
402
|
+
### Added
|
|
403
|
+
|
|
404
|
+
- `unknown-sync-option` rule: an Application-level `syncOptions` entry
|
|
405
|
+
that doesn't match any ArgoCD-recognized key.
|
|
406
|
+
|
|
407
|
+
## [0.1.16] - 2026-09-18
|
|
408
|
+
|
|
409
|
+
### Added
|
|
410
|
+
|
|
411
|
+
- `duplicate-application-name` rule: two or more Applications sharing
|
|
412
|
+
the same namespace+name.
|
|
413
|
+
- `malformed-ignore-diff-pointer` rule: an `ignoreDifferences`
|
|
414
|
+
`jsonPointers` entry that doesn't start with `/` (RFC 6901).
|
|
415
|
+
|
|
416
|
+
## [0.1.15] - 2026-09-18
|
|
417
|
+
|
|
418
|
+
### Added
|
|
419
|
+
|
|
420
|
+
- `sync-validation-disabled` rule: `Validate=false` in `syncOptions`.
|
|
421
|
+
|
|
422
|
+
### Fixed
|
|
423
|
+
|
|
424
|
+
- The ApplicationSet `matrix` generator is now capped at 2 child
|
|
425
|
+
generators, matching ArgoCD's own limit — previously computed a bogus
|
|
426
|
+
N-way cartesian product beyond that.
|
|
427
|
+
|
|
428
|
+
## [0.1.14] - 2026-09-18
|
|
429
|
+
|
|
430
|
+
### Added
|
|
431
|
+
|
|
432
|
+
- `hpa-selfheal-conflict` rule: a `HorizontalPodAutoscaler` and
|
|
433
|
+
`selfHeal: true` both managing `spec.replicas` without
|
|
434
|
+
`ignoreDifferences` and the `RespectIgnoreDifferences` sync option.
|
|
435
|
+
|
|
436
|
+
## [0.1.13] - 2026-09-18
|
|
437
|
+
|
|
438
|
+
### Added
|
|
439
|
+
|
|
440
|
+
- Stale baseline entry detection: a `--write-baseline` entry no longer
|
|
441
|
+
matching any current finding is reported as a count, without
|
|
442
|
+
affecting the exit code.
|
|
443
|
+
- JUnit XML reporter (`--format junit`).
|
|
444
|
+
|
|
445
|
+
## [0.1.12] - 2026-09-18
|
|
446
|
+
|
|
447
|
+
### Added
|
|
448
|
+
|
|
449
|
+
- Elastic ECK, RabbitMQ Cluster Operator and Strimzi signatures to the
|
|
450
|
+
built-in `known_operators` pack.
|
|
451
|
+
|
|
452
|
+
## [0.1.11] - 2026-09-18
|
|
453
|
+
|
|
454
|
+
### Added
|
|
455
|
+
|
|
456
|
+
- `project-scope-violation` rule: an Application's source/destination
|
|
457
|
+
outside its AppProject's `sourceRepos`/`destinations` scope.
|
|
458
|
+
|
|
459
|
+
## [0.1.10] - 2026-09-18
|
|
460
|
+
|
|
461
|
+
### Added
|
|
462
|
+
|
|
463
|
+
- `revision-mismatch` rule (info): a source's `targetRevision` differs
|
|
464
|
+
from the checked-out revision.
|
|
465
|
+
|
|
466
|
+
### Fixed
|
|
467
|
+
|
|
468
|
+
- `orphan-source`, `missing-ignore-diff` and `double-coverage` now
|
|
469
|
+
resolve a source against its own `targetRevision` when it drifts from
|
|
470
|
+
HEAD, instead of always reading the checked-out working tree — a
|
|
471
|
+
manifest only live at a pinned revision was previously never actually
|
|
472
|
+
read.
|
|
473
|
+
|
|
474
|
+
## [0.1.9] - 2026-09-18
|
|
475
|
+
|
|
476
|
+
### Added
|
|
477
|
+
|
|
478
|
+
- Support for the ApplicationSet `merge` generator (previously always
|
|
479
|
+
flagged `unresolvable-generator`).
|
|
480
|
+
|
|
481
|
+
## [0.1.8] - 2026-09-15
|
|
482
|
+
|
|
483
|
+
### Fixed
|
|
484
|
+
|
|
485
|
+
- An ApplicationSet generator's own `selector` (label filter) is now
|
|
486
|
+
flagged `unresolvable-generator` instead of silently ignored.
|
|
487
|
+
|
|
488
|
+
## [0.1.7] - 2026-09-15
|
|
489
|
+
|
|
490
|
+
### Added
|
|
491
|
+
|
|
492
|
+
- `double-coverage` rule: a file covered by more than one different
|
|
493
|
+
Application's sources at once.
|
|
494
|
+
|
|
495
|
+
## [0.1.6] - 2026-09-15
|
|
496
|
+
|
|
497
|
+
Initial public release, published under the `gillesl-dev` GitHub/PyPI
|
|
498
|
+
namespace.
|
|
499
|
+
|
|
500
|
+
### Added
|
|
501
|
+
|
|
502
|
+
- Core rules: `orphan-source`, `broken-values-ref`,
|
|
503
|
+
`missing-ignore-diff`, `phantom-target`, `unresolvable-generator`.
|
|
504
|
+
|
|
505
|
+
### Fixed
|
|
506
|
+
|
|
507
|
+
- The table reporter now shows a placeholder instead of a blank cell
|
|
508
|
+
for `orphan-source` findings, which have no owning Application.
|