wow-secret-lint 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/README.md +63 -14
- package/action/index.mjs +16 -2
- package/action.yml +4 -0
- package/bin/wow-secret-lint.mjs +8 -1
- package/package.json +2 -1
- package/src/analyze.mjs +233 -15
- package/src/index.mjs +7 -2
- package/src/report.mjs +1 -0
- package/src/rules.mjs +177 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.3.0 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
Patch 12.1 ("Curse of Ula'tek", live 2026-08-11) widened the secret surface far beyond
|
|
6
|
+
12.0, and 1.2.0 reported a false clean on the most common aura pattern in retail addons.
|
|
7
|
+
This release adds the 12.1 surface and a `--patch` flag to pin the old one.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `--patch=<12.0|12.1>` (default `12.1`), also exposed as the `patch` input of the GitHub
|
|
12
|
+
Action. `12.0` reproduces the 1.2.0 rule surface byte for byte, for addons still
|
|
13
|
+
targeting the older client.
|
|
14
|
+
- **WSL012** (error): forbidden operation on a secret aura vector. Taints the returns of
|
|
15
|
+
`C_UnitAuras.GetUnitAuras`, `GetUnitAuraInstanceIDs`, `GetAuraSlots`, the
|
|
16
|
+
by-index/slot/instance aura queries and the by-spell lookups; flags `#`,
|
|
17
|
+
`pairs`/`ipairs` iteration, indexing and table-key use. Boolean-testing the return stays
|
|
18
|
+
silent, because nil-checking it is the sanctioned pattern, and the shipped guard idiom
|
|
19
|
+
(`if not aura or issecretvalue(aura.name) then return end`, as in DBM and BigWigs) is
|
|
20
|
+
credited in full.
|
|
21
|
+
- **WSL013** (error): forbidden operation on a secret unit identity value, covering
|
|
22
|
+
`UnitClass`, `UnitClassBase`, `UnitRace`, `UnitSex`, `UnitSexBase`, `UnitIsCharmed` and
|
|
23
|
+
`UnitIsPossessed`, routed through the existing arithmetic, comparison and boolean-test
|
|
24
|
+
checkers. Literal `"player"` (and `"player"`/`"pet"`/`"vehicle"` for the charm pair, per
|
|
25
|
+
the 12.1 notes) is exempt.
|
|
26
|
+
- **WSL014** (error): removed or renamed symbols, each message naming the replacement:
|
|
27
|
+
`SecureAuraHeaderTemplate`, `C_UnitAuras.TriggerPrivateAuraShowDispelType`,
|
|
28
|
+
`CanAccessObject`, `UIParentLoadAddOn`.
|
|
29
|
+
- **WSL015** (warning): deprecated `getglobal`/`setglobal`.
|
|
30
|
+
- **WSL016** (warning): `showCountdownFrame` passed to a private-aura API. The field was
|
|
31
|
+
renamed to `showCooldownFrame` and the old name is silently ignored, so the cooldown
|
|
32
|
+
swipe disappears with no Lua error. Caught inline and through a local args table.
|
|
33
|
+
- **WSL017** (error): forbidden-aspect operations on AuraButtons and AuraContainers:
|
|
34
|
+
script handler installation, event registration, input calls and focus queries, matched
|
|
35
|
+
to the UntrustedScriptExecution, EventRegistrations, ScriptedInput and QueryFocus
|
|
36
|
+
aspects. AuraButtons are recognised from `CreateFrame` and from `initializeFrame`
|
|
37
|
+
callbacks; construction itself is never flagged.
|
|
38
|
+
|
|
39
|
+
### Measured
|
|
40
|
+
|
|
41
|
+
On the same 12-addon corpus as 1.2.0: `--patch=12.0` reproduces 1.2.0 exactly (20 errors,
|
|
42
|
+
90 warnings, 3 of 12 failing). The default 12.1 surface reports 273 errors and 117
|
|
43
|
+
warnings, failing 8 of 12, dominated by WSL012 (148) and WSL013 (104). DBM and BigWigs
|
|
44
|
+
report zero WSL012 because both already guard every aura lookup; the findings sit in the
|
|
45
|
+
addons that have not done that work yet. WSL016 catches the `showCountdownFrame` in oUF's
|
|
46
|
+
`privateauras.lua` and both vendored copies of it (KkthnxUI, SpartanUI).
|
|
47
|
+
|
|
48
|
+
WSL012 and WSL013 gate builds without `--strict`. Unlike the `SecretReturns` tier, they
|
|
49
|
+
rest on the 12.1 notes stating the behaviour outright ("will now either return full
|
|
50
|
+
secrets or nil when called by addons") rather than on a documentation marker.
|
|
51
|
+
|
|
52
|
+
### Clone check
|
|
53
|
+
|
|
54
|
+
The six rules reuse the existing machinery rather than shipping parallel bodies: WSL012
|
|
55
|
+
and WSL013 are taint seeds routed through the existing operator checkers via a one-line
|
|
56
|
+
rule-resolution step, and WSL014/WSL015 share one table-driven removed-call check. The
|
|
57
|
+
new standalone bodies (WSL016 field tracking, WSL017 widget tagging) parallel nothing in
|
|
58
|
+
the codebase; highest pairwise similarity across all new functions is below 40%.
|
|
59
|
+
|
|
60
|
+
## 1.2.0 - 2026-08-24
|
|
61
|
+
|
|
62
|
+
- Flavour-aware `.toc` discovery: descends up to three levels, skips folders whose every
|
|
63
|
+
`.toc` targets Classic, announces blind walks. Fixed WeakAuras-style repos silently
|
|
64
|
+
bypassing the `.toc` mechanism. Classic contamination in strict findings: 8 of 118 to 0
|
|
65
|
+
of 110.
|
|
66
|
+
|
|
67
|
+
## 1.1.0 - 2026-08-24
|
|
68
|
+
|
|
69
|
+
- Only `WSL008` fails a build by default; everything resting on `SecretReturns` reports
|
|
70
|
+
as a warning, with `--strict` to raise it. The default is correct under either reading
|
|
71
|
+
of the documentation.
|
|
72
|
+
|
|
73
|
+
## 1.0.0 - 2026-08-24
|
|
74
|
+
|
|
75
|
+
- First release: rules WSL001-WSL011, taint tracking, guard detection, `.toc`/XML
|
|
76
|
+
resolution, three reporters, GitHub Action, vendored snapshot of Blizzard's generated
|
|
77
|
+
API documentation.
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# wow-secret-lint
|
|
2
2
|
|
|
3
|
-
Static analysis for World of Warcraft **retail** addons. It maps where your Lua touches Blizzard's secret-value API, and hard-fails your build on the
|
|
3
|
+
Static analysis for World of Warcraft **retail** addons. It maps where your Lua touches Blizzard's secret-value API, and hard-fails your build on the things Blizzard documents as certain to break.
|
|
4
4
|
|
|
5
|
-
Patch 12.0 introduced [secret values](https://warcraft.wiki.gg/wiki/Secret_Values). A lot of the API now hands your addon a value you are allowed to store, pass around and print, but not to do arithmetic on, compare, index, call, or measure with `#`. When tainted code does
|
|
5
|
+
Patch 12.0 introduced [secret values](https://warcraft.wiki.gg/wiki/Secret_Values). A lot of the API now hands your addon a value you are allowed to store, pass around and print, but not to do arithmetic on, compare, index, call, or measure with `#`. Patch 12.1 (Curse of Ula'tek, live 2026-08-11) [widened that surface enormously](https://warcraft.wiki.gg/wiki/Patch_12.1.0/API_changes): every `UnitAura` API returns full secrets or nil while auras are secret, seven unit identity APIs joined them, and the new AuraButton/AuraContainer frames carry forbidden aspects. When tainted code does something disallowed, the wiki is blunt about the result:
|
|
6
6
|
|
|
7
7
|
> When an operation that is not allowed is performed, the result will be an **immediate** Lua error.
|
|
8
8
|
|
|
@@ -17,7 +17,7 @@ That one is [KkthnxUI#121](https://github.com/Kkthnx-Wow/KkthnxUI/issues/121), f
|
|
|
17
17
|
|
|
18
18
|
`wow-secret-lint` reads Blizzard's own generated API documentation, tracks which of your locals hold a secret, and tells you where you touch one.
|
|
19
19
|
|
|
20
|
-
**It reports, it does not accuse.**
|
|
20
|
+
**It reports, it does not accuse.** The rules that fail your build by default are the ones Blizzard states outright: `WSL008` (combat log registration errors), and the 12.1 rules `WSL012`/`WSL013` (aura and identity APIs "will now either return full secrets or nil when called by addons"), `WSL014` (the symbol no longer exists) and `WSL017` (forbidden aspects). The 12.0-era `SecretReturns` tier stays a warning, because whether those APIs really hand a secret to tainted code on every call is not something static analysis can confirm; run `--strict` to gate on it. [The reasoning is spelled out below](#the-open-question-on-severity) and it is worth two minutes before you wire this into CI.
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
@@ -74,6 +74,25 @@ $ npx wow-secret-lint Core/Guarded.lua
|
|
|
74
74
|
|
|
75
75
|
Exit code 0.
|
|
76
76
|
|
|
77
|
+
### The 12.1 aura surface
|
|
78
|
+
|
|
79
|
+
The most common aura pattern in retail addons stopped working in 12.1. Per the [12.1 API changes](https://warcraft.wiki.gg/wiki/Patch_12.1.0/API_changes): *"APIs like GetUnitAuras and GetUnitAuraInstanceIDs will return a secret vector, meaning addon code will not be able to determine how many auras it contains or iterate through it."* `Core/Auras.lua`:
|
|
80
|
+
|
|
81
|
+
```lua
|
|
82
|
+
local auras = C_UnitAuras.GetUnitAuras('player'); for i = 1, #auras do print(auras[i].name) end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
$ npx wow-secret-lint Core/Auras.lua
|
|
87
|
+
Core/Auras.lua:1:63 error WSL012 length operator (#) on a secret value: 'auras' derives from C_UnitAuras.GetUnitAuras(), secret in 12.1 while auras are secret (combat, encounters, M+, PvP); show auras with an AuraContainer (AddAuraGroup/AddAuraSlot) instead
|
|
88
|
+
Core/Auras.lua:1:78 error WSL012 indexed access on a secret value: 'auras' derives from C_UnitAuras.GetUnitAuras(), secret in 12.1 while auras are secret (combat, encounters, M+, PvP); show auras with an AuraContainer (AddAuraGroup/AddAuraSlot) instead
|
|
89
|
+
2 errors, 0 warnings
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Exit code 1. Under `--patch=12.0` the same file reports zero findings and exits 0, because the pre-12.1 API really did return an iterable table. `if auras then` stays silent under both, since nil-checking the return is the sanctioned pattern.
|
|
93
|
+
|
|
94
|
+
None of this comes from Blizzard's generated documentation, which carries no marker for aura secrecy. The 12.1 behaviour is published only in the patch notes, so these rules seed their taint from a list transcribed there, and `--patch` selects whether that list is active.
|
|
95
|
+
|
|
77
96
|
### Exit codes
|
|
78
97
|
|
|
79
98
|
| Code | Meaning |
|
|
@@ -107,6 +126,7 @@ Inputs:
|
|
|
107
126
|
| Input | Default | Description |
|
|
108
127
|
| --- | --- | --- |
|
|
109
128
|
| `path` | `.` | addon folder, `.toc`, or `.lua`. Several allowed, space separated |
|
|
129
|
+
| `patch` | `12.1` | patch surface to check; `12.0` pins the pre-12.1 rule set |
|
|
110
130
|
| `args` | `""` | extra CLI arguments, e.g. `--conditional=warn --disable=WSL011` |
|
|
111
131
|
| `format` | `github` | `github`, `stylish`, or `json` |
|
|
112
132
|
|
|
@@ -114,7 +134,7 @@ Outputs: `errors`, `warnings`.
|
|
|
114
134
|
|
|
115
135
|
## Rules
|
|
116
136
|
|
|
117
|
-
Every rule maps to one sentence Blizzard publishes. `wow-secret-lint --rules` prints the table with the source for each.
|
|
137
|
+
Every rule maps to one sentence Blizzard publishes. `wow-secret-lint --rules` prints the table with the source for each. WSL008, WSL012, WSL013, WSL014 and WSL017 fail a build by default; the rest are warnings.
|
|
118
138
|
|
|
119
139
|
| Rule | Severity | What it catches |
|
|
120
140
|
| --- | --- | --- |
|
|
@@ -129,11 +149,29 @@ Every rule maps to one sentence Blizzard publishes. `wow-secret-lint --rules` pr
|
|
|
129
149
|
| WSL009 | warning | secret crosses into a function in this file that never guards it |
|
|
130
150
|
| WSL010 | warning | conditionally secret value used with no guard anywhere in scope |
|
|
131
151
|
| WSL011 | warning | `tostring()` on a secret value |
|
|
152
|
+
| WSL012 | error | 12.1: `#`, iteration, indexing or table-key use of a secret aura vector |
|
|
153
|
+
| WSL013 | error | 12.1: arithmetic, comparison or boolean test on a secret unit identity value |
|
|
154
|
+
| WSL014 | error | 12.1: removed or renamed symbol; each message names the replacement |
|
|
155
|
+
| WSL015 | warning | 12.1: deprecated `getglobal`/`setglobal` |
|
|
156
|
+
| WSL016 | warning | 12.1: `showCountdownFrame` passed to a private-aura API |
|
|
157
|
+
| WSL017 | error | 12.1: forbidden-aspect operation on an AuraButton or AuraContainer |
|
|
132
158
|
|
|
133
159
|
WSL008 comes from the [12.0.0 API changes](https://warcraft.wiki.gg/wiki/Patch_12.0.0/API_changes): *"COMBAT_LOG_EVENT and COMBAT_LOG_EVENT_UNFILTERED will error when trying to register them."* Use `COMBAT_LOG_EVENT_INTERNAL_UNFILTERED` or the `C_CombatLog` namespace.
|
|
134
160
|
|
|
135
161
|
WSL011 is a warning on purpose. `tostring` is absent from the wiki's allowed list but is not listed as forbidden either, so it stays a warning until someone confirms it in game.
|
|
136
162
|
|
|
163
|
+
### The 12.1 rules in detail
|
|
164
|
+
|
|
165
|
+
**WSL012** taints `C_UnitAuras.GetUnitAuras`, `GetUnitAuraInstanceIDs`, `GetAuraSlots`, the by-index/slot/instance queries (`GetAuraDataByIndex`, `GetAuraDataBySlot`, `GetAuraDataByAuraInstanceID`, `GetBuffDataByIndex`, `GetDebuffDataByIndex`) and the by-spell lookups (`GetAuraDataBySpellName`, `GetUnitAuraBySpellID`, `GetPlayerAuraBySpellID`, `GetCooldownAuraBySpellID`), since the notes say all of the UnitAura APIs return full secrets or nil. The by-index family additionally Lua-errors at the call itself while auras are secret, so any downstream use of its result is broken either way. Boolean tests on aura returns are never flagged: the value is a table or nil, and nil-checking it is how the new contract is meant to be consumed. The guard idiom DBM and BigWigs actually ship, `if not aura or issecretvalue(aura.name) then return end`, is credited in full: indexing one field inside a guard call is the test itself, and a guard naming any field vouches for the whole aura. Auras Blizzard explicitly flags as non-secret still come back readable; static analysis cannot know which spell IDs those are, so an unguarded read of one is still reported.
|
|
166
|
+
|
|
167
|
+
**WSL013** covers the seven unit APIs the 12.1 notes single out: `UnitClass`, `UnitClassBase`, `UnitRace`, `UnitSex`, `UnitSexBase`, plus `UnitIsCharmed`/`UnitIsPossessed` which follow aura secrecy. The findings come out of the same arithmetic, comparison and boolean-test checkers as WSL001/002/007; only the rule id and severity differ. A literal `"player"` token is exempt (a unit is never secret to itself), and the charm pair is exempt for `"player"`, `"pet"` and `"vehicle"`, which the 12.1 notes state outright. A variable token is not exempt, because it can name an arena opponent. The wider identity set from the notes (`UnitGroupRolesAssigned`, `UnitInRaid`, `UnitIsPVP` and friends) stays in the conditional tier for now: turn on `--conditional=warn` to see it.
|
|
168
|
+
|
|
169
|
+
**WSL014** flags calls to `UIParentLoadAddOn` (renamed `LoadAddOnWithErrorHandling`), `CanAccessObject` (use `FrameScriptObject:CanBeAccessedInContext`), `C_UnitAuras.TriggerPrivateAuraShowDispelType` (removed; use the `showDispelIcon` anchor option) and any `CreateFrame` template list naming `SecureAuraHeaderTemplate` (removed from Mainline; migrate to an AuraContainer). Referencing `UIParentLoadAddOn` without calling it is not flagged, because `if UIParentLoadAddOn then` is how you feature-detect the old client.
|
|
170
|
+
|
|
171
|
+
**WSL016** exists because this one fails silently: `showCountdownFrame` was renamed `showCooldownFrame` in the private-aura anchor structures, the old field is ignored, and the cooldown swipe just stops appearing with no error to trace. It is caught both inline and through a local args table. oUF's `privateauras.lua` ships this bug today, as do the copies vendored into KkthnxUI and SpartanUI.
|
|
172
|
+
|
|
173
|
+
**WSL017** knows a local is an AuraContainer or AuraButton when it comes from `CreateFrame("AuraContainer", ...)`/`CreateFrame("AuraButton", ...)` or arrives as the `initializeFrame` callback parameter of `AddAuraGroup`/`AddAuraSlot`/`AddItemEnchantment`. On those it flags `SetScript`/`HookScript`, event registration, input calls (`EnableMouse`, `RegisterForClicks`, `Click`, ...) and focus queries (`IsMouseOver`, ...), naming the forbidden aspect each one trips. Construction itself is never flagged.
|
|
174
|
+
|
|
137
175
|
### What it will never flag
|
|
138
176
|
|
|
139
177
|
The wiki has an explicit allowed list, and flagging any of it is a false positive that gets a linter uninstalled. Each of these has a passing negative test in `test/`:
|
|
@@ -165,7 +203,7 @@ npx wow-secret-lint ./MyAddon --secret-guard=IsLocked --access-guard=CanRead
|
|
|
165
203
|
|
|
166
204
|
Blizzard marks a second tier in the generated docs: functions that return a secret only while a specific restriction is active. `C_Spell.GetSpellCooldown` carries `SecretWhenCooldownsRestricted`; `C_LFGList.GetSearchResultInfo` carries `SecretInChatMessagingLockdown`; `UnitGUID`, `UnitName` and `UnitClass` carry `SecretWhenUnitIdentityRestricted`. In practice that means "secret in PvP, in restricted instances, and for non-player or pet units in combat". Your code works fine right up until it does not.
|
|
167
205
|
|
|
168
|
-
|
|
206
|
+
Under the default `--patch=12.1`, the seven identity APIs WSL013 covers are promoted out of this tier and report as errors regardless of the `--conditional` setting. The rest of the tier is **off by default**, because measured against 12 real addons it produces about one warning per Lua file, which is a tax rather than a signal. Turn it on for a deeper audit:
|
|
169
207
|
|
|
170
208
|
```bash
|
|
171
209
|
npx wow-secret-lint ./MyAddon --conditional=warn # report as warnings
|
|
@@ -194,23 +232,29 @@ That is [BtWLoadouts#67](https://github.com/Breeni/BtWLoadouts/issues/67), reduc
|
|
|
194
232
|
|
|
195
233
|
## Measured on real addons
|
|
196
234
|
|
|
197
|
-
Run against 12 real retail addons (BigWigs, LittleWigs, DBM, WeakAuras, Details, SpartanUI, KkthnxUI, oUF, Bartender4, Premade Groups Filter, BtWLoadouts, AdvancedInterfaceOptions),
|
|
235
|
+
Run against 12 real retail addons (BigWigs, LittleWigs, DBM, WeakAuras, Details, SpartanUI, KkthnxUI, oUF, Bartender4, Premade Groups Filter, BtWLoadouts, AdvancedInterfaceOptions), 1,997 Lua files reachable from a retail `.toc`:
|
|
198
236
|
|
|
199
237
|
| Mode | Errors | Warnings | Addons that would fail CI |
|
|
200
238
|
| --- | --- | --- | --- |
|
|
201
|
-
| default |
|
|
202
|
-
| `--strict` |
|
|
239
|
+
| `--patch=12.1` (default) | 273 | 117 | 8 of 12 |
|
|
240
|
+
| `--patch=12.1 --strict` | 383 | 7 | 8 of 12 |
|
|
241
|
+
| `--patch=12.0` | 20 | 90 | 3 of 12 |
|
|
242
|
+
| `--patch=12.0 --strict` | 110 | 0 | 5 of 12 |
|
|
243
|
+
|
|
244
|
+
The two `12.0` rows are identical to what v1.2.0 reported, which is the point of the flag. The jump to 273 is the 12.1 aura and identity surface landing: 148 WSL012 and 104 WSL013 findings, concentrated exactly where the [PTR forum thread](https://us.forums.blizzard.com/en/wow/t/minicc-and-similar-addons-might-be-partially-broken-in-121/2310937) predicted breakage: Details' aura scanning (80 WSL012), SpartanUI's aura designer and corner indicators (56), oUF's classpower element and the copy of it inside KkthnxUI (5 each), DBM's class-keyed tables (50 WSL013), BigWigs (20 WSL013). WSL016 catches the silently dead `showCountdownFrame` in oUF `privateauras.lua:133` and both copies of it vendored into KkthnxUI and SpartanUI; WSL014 catches SpartanUI calling the renamed `UIParentLoadAddOn`. No addon in the corpus uses AuraContainers yet, so WSL017 measures 0 there and is exercised by fixtures.
|
|
203
245
|
|
|
204
|
-
|
|
246
|
+
The number worth noticing is the one that is zero: **DBM and BigWigs report no WSL012 at all**, because both already wrap every aura lookup in an `issecretvalue` guard, and the analysis credits that idiom (see below). The linter separates code that has done the 12.1 work from code that has not.
|
|
205
247
|
|
|
206
|
-
|
|
248
|
+
A sample of the WSL012/WSL013 findings across every affected repo was read against its source by hand; each one performs an operation the 12.1 notes document as disallowed, on a value those notes document as secret, with no guard in scope. The pre-12.1 findings are unchanged from v1.2.0, where all 110 strict findings were read by hand.
|
|
207
249
|
|
|
208
|
-
|
|
250
|
+
Contamination from Classic-flavour code is 0, checked by resolving every retail `.toc` in each repo and matching it against the findings.
|
|
209
251
|
|
|
210
|
-
SpartanUI's `libs/oUF_Classic` and `libs/LibClassicDurations` do get flagged and that is correct: its single `.toc` declares `## Interface: 120100, 50504, 38002, 20506, 11509`, so those libraries are listed for retail too. Blizzard's own `BlizzardInterfaceCode` is in the corpus only as a parser stress test, 2,
|
|
252
|
+
SpartanUI's `libs/oUF_Classic` and `libs/LibClassicDurations` do get flagged and that is correct: its single `.toc` declares `## Interface: 120100, 50504, 38002, 20506, 11509`, so those libraries are listed for retail too. Blizzard's own `BlizzardInterfaceCode` is in the corpus only as a parser stress test, 2,128 retail files with 0 parse errors, since its code runs untainted and cannot violate these rules.
|
|
211
253
|
|
|
212
254
|
## The open question on severity
|
|
213
255
|
|
|
256
|
+
This section is about the 12.0-era `SecretReturns` tier (WSL001-WSL007 on `UnitHealth` and friends), which stays a warning by default. It does not apply to WSL012/WSL013: those rest on the 12.1 notes stating the behaviour in plain language, not on a documentation marker whose runtime meaning is unconfirmed.
|
|
257
|
+
|
|
214
258
|
The Secret Values page says functions marked `SecretReturns = true` "unconditionally return secret values", and that is what the error tier is built on. There is evidence pulling the other way and you should know about it.
|
|
215
259
|
|
|
216
260
|
Searching the issue trackers of DeadlyBossMods, BigWigs, Details and WeakAuras finds **zero** reports mentioning `UnitHealth` and secret values, while this tool flags dozens of `UnitHealth`-derived findings across them. If `UnitHealth` really did hand a secret to tainted code on every call, `UnitHealth(uId) / UnitHealthMax(uId) * 100` in DBM-Core would throw on every boss pull for millions of users, and the trackers would show it.
|
|
@@ -229,6 +273,8 @@ Settling it needs someone to run a flagged line in game and watch what happens,
|
|
|
229
273
|
```
|
|
230
274
|
--format=<stylish|json|github> output format (default: stylish)
|
|
231
275
|
--game=<retail|classic> classic has no secret values and exits 0 immediately
|
|
276
|
+
--patch=<12.0|12.1> patch surface the built-in rules check (default: 12.1);
|
|
277
|
+
12.0 pins the pre-12.1 rule set
|
|
232
278
|
--strict raise SecretReturns findings from warning to error
|
|
233
279
|
--conditional=<off|warn|error> conditionally secret APIs (default: off)
|
|
234
280
|
--secret-guard=<names> extra is-secret wrapper functions, comma separated
|
|
@@ -252,6 +298,8 @@ npx wow-secret-lint --refresh
|
|
|
252
298
|
|
|
253
299
|
A scheduled workflow in this repo does that weekly and opens a pull request when the docs move.
|
|
254
300
|
|
|
301
|
+
The 12.1 aura and identity secrecy is deliberately **not** part of the snapshot: Blizzard's generated documentation carries no marker for it, only the patch notes describe it, so the WSL012/WSL013 API lists live in `src/rules.mjs` with the source quoted, and `--patch` gates them. There is one snapshot, and `--snapshot`/`--refresh` behave the same under either patch.
|
|
302
|
+
|
|
255
303
|
## Limitations and non-goals
|
|
256
304
|
|
|
257
305
|
- **No auto-fixing.** It tells you where; the fix is yours.
|
|
@@ -259,7 +307,8 @@ A scheduled workflow in this repo does that weekly and opens a pull request when
|
|
|
259
307
|
- **No Classic support.** Classic has no secret values, so `--game=classic` exits 0 immediately.
|
|
260
308
|
- **No cross-file interprocedural analysis in v1.** Taint follows plain assignment, table field stores, the return value of a file-local function, and one level of intra-file call-argument passing. A secret that leaves through a global and comes back in another file is not tracked.
|
|
261
309
|
- **No LuaJIT or Lua 5.4 syntax.** Files are parsed as Lua 5.1. WoW accepts a semicolon after `break`, which stock 5.1 does not, so a file that fails on 5.1 gets one retry under the 5.2 grammar before it is reported as a parse error.
|
|
262
|
-
- **Method calls are not resolved to a widget type**, so WSL006 only applies to plain and namespaced calls (`UnitHealth(...)`, `C_CVar.SetCVar(...)`), never to `frame:SetText(...)`.
|
|
310
|
+
- **Method calls are not resolved to a widget type**, so WSL006 only applies to plain and namespaced calls (`UnitHealth(...)`, `C_CVar.SetCVar(...)`), never to `frame:SetText(...)`. WSL017 is the one exception, and its typing is deliberately narrow: a local counts as an AuraContainer/AuraButton only when it comes straight from `CreateFrame` with a literal type string or from an `initializeFrame` callback. A button stored in a table field or passed across files is not tracked.
|
|
311
|
+
- **XML is followed for `<Script>`/`<Include>`, not linted.** A `SecureAuraHeaderTemplate` referenced from an XML `inherits` attribute is invisible to WSL014; only Lua-side `CreateFrame` template strings are checked.
|
|
263
312
|
- **`string.format` output is not tracked as secret.** The wiki names it as the sanctioned way to render a secret, and following it would flood every `SetText` call site. The trade is a known blind spot on `#string.format(...)`.
|
|
264
313
|
- A `.toc` listing a file that is not on disk warns and keeps going. A file that will not parse is reported and the run exits 2.
|
|
265
314
|
|
|
@@ -269,7 +318,7 @@ A scheduled workflow in this repo does that weekly and opens a pull request when
|
|
|
269
318
|
npm test
|
|
270
319
|
```
|
|
271
320
|
|
|
272
|
-
|
|
321
|
+
173 tests. The suite covers every rule, the guard forms, the permitted-operations negative cases, the three reporters, the CLI surface, one violating and one clean fixture per 12.1 rule (`test/fixtures/rules-121/`), a fixture proving `--patch=12.0` reproduces the v1.2.0 output byte for byte (`test/fixtures/patch/`), and eight regression fixtures reconstructed from real shipped traces:
|
|
273
322
|
|
|
274
323
|
| Fixture | Issue |
|
|
275
324
|
| --- | --- |
|
package/action/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import process from 'node:process';
|
|
9
9
|
import { lintPaths, VERSION } from '../src/index.mjs';
|
|
10
10
|
import { format, FORMATS, counts } from '../src/report.mjs';
|
|
11
|
-
import { RULE_IDS } from '../src/rules.mjs';
|
|
11
|
+
import { RULE_IDS, PATCHES, DEFAULT_PATCH } from '../src/rules.mjs';
|
|
12
12
|
|
|
13
13
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
|
|
@@ -49,7 +49,15 @@ const fmt = input('format', 'github');
|
|
|
49
49
|
|
|
50
50
|
if (!FORMATS.includes(fmt)) fail(`unknown format "${fmt}" (expected one of: ${FORMATS.join(', ')})`);
|
|
51
51
|
|
|
52
|
-
const options = {
|
|
52
|
+
const options = {
|
|
53
|
+
conditional: 'off',
|
|
54
|
+
strict: false,
|
|
55
|
+
disable: [],
|
|
56
|
+
secretGuards: [],
|
|
57
|
+
accessGuards: [],
|
|
58
|
+
game: 'retail',
|
|
59
|
+
patch: input('patch', DEFAULT_PATCH),
|
|
60
|
+
};
|
|
53
61
|
let maxWarnings = Infinity;
|
|
54
62
|
|
|
55
63
|
for (let i = 0; i < extra.length; i++) {
|
|
@@ -68,6 +76,9 @@ for (let i = 0; i < extra.length; i++) {
|
|
|
68
76
|
case '--game':
|
|
69
77
|
options.game = val();
|
|
70
78
|
break;
|
|
79
|
+
case '--patch':
|
|
80
|
+
options.patch = val();
|
|
81
|
+
break;
|
|
71
82
|
case '--disable':
|
|
72
83
|
options.disable.push(...val().split(',').map((s) => s.trim()).filter(Boolean));
|
|
73
84
|
break;
|
|
@@ -94,6 +105,9 @@ if (!['off', 'warn', 'error'].includes(options.conditional)) {
|
|
|
94
105
|
if (!['retail', 'classic'].includes(options.game)) {
|
|
95
106
|
fail(`unknown --game "${options.game}" (expected retail or classic)`);
|
|
96
107
|
}
|
|
108
|
+
if (!PATCHES.includes(options.patch)) {
|
|
109
|
+
fail(`unknown patch "${options.patch}" (expected ${PATCHES.join(' or ')})`);
|
|
110
|
+
}
|
|
97
111
|
for (const id of options.disable) {
|
|
98
112
|
if (!RULE_IDS.includes(id)) fail(`unknown rule id "${id}" in --disable`);
|
|
99
113
|
}
|
package/action.yml
CHANGED
|
@@ -10,6 +10,10 @@ inputs:
|
|
|
10
10
|
description: 'Addon folder, .toc, or .lua file to lint. Accepts several, space separated.'
|
|
11
11
|
required: false
|
|
12
12
|
default: '.'
|
|
13
|
+
patch:
|
|
14
|
+
description: 'Patch surface to check: 12.1 (default) or 12.0 for addons pinned to the older client.'
|
|
15
|
+
required: false
|
|
16
|
+
default: '12.1'
|
|
13
17
|
args:
|
|
14
18
|
description: 'Extra CLI arguments, e.g. "--conditional=warn --disable=WSL011".'
|
|
15
19
|
required: false
|
package/bin/wow-secret-lint.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import process from 'node:process';
|
|
|
7
7
|
import { lintPaths, VERSION } from '../src/index.mjs';
|
|
8
8
|
import { format, FORMATS, counts } from '../src/report.mjs';
|
|
9
9
|
import { refreshSnapshot, writeSnapshot, SNAPSHOT_PATH } from '../src/apidata.mjs';
|
|
10
|
-
import { RULES, RULE_IDS } from '../src/rules.mjs';
|
|
10
|
+
import { RULES, RULE_IDS, PATCHES, DEFAULT_PATCH } from '../src/rules.mjs';
|
|
11
11
|
|
|
12
12
|
const USAGE = `wow-secret-lint ${VERSION}
|
|
13
13
|
Static analysis for World of Warcraft retail addons: finds Secret Value violations
|
|
@@ -21,6 +21,9 @@ Usage:
|
|
|
21
21
|
Options:
|
|
22
22
|
--format=<stylish|json|github> output format (default: stylish)
|
|
23
23
|
--game=<retail|classic> classic has no secret values and exits 0 immediately
|
|
24
|
+
--patch=<12.0|12.1> which patch surface the built-in rules check
|
|
25
|
+
(default: 12.1). 12.0 pins the pre-12.1 rule set for
|
|
26
|
+
addons still targeting the older client
|
|
24
27
|
--strict raise SecretReturns findings from warning to error.
|
|
25
28
|
Off by default: see "the open question on severity"
|
|
26
29
|
in the README before you gate CI on them.
|
|
@@ -52,6 +55,7 @@ function parseArgs(argv) {
|
|
|
52
55
|
const opts = {
|
|
53
56
|
format: 'stylish',
|
|
54
57
|
game: 'retail',
|
|
58
|
+
patch: DEFAULT_PATCH,
|
|
55
59
|
conditional: 'off',
|
|
56
60
|
strict: false,
|
|
57
61
|
disable: [],
|
|
@@ -78,6 +82,7 @@ function parseArgs(argv) {
|
|
|
78
82
|
else if (arg === '--strict') opts.strict = true;
|
|
79
83
|
else if (arg.startsWith('--format')) opts.format = value(arg, argv, () => i++);
|
|
80
84
|
else if (arg.startsWith('--game')) opts.game = value(arg, argv, () => i++);
|
|
85
|
+
else if (arg.startsWith('--patch')) opts.patch = value(arg, argv, () => i++);
|
|
81
86
|
else if (arg.startsWith('--conditional')) opts.conditional = value(arg, argv, () => i++);
|
|
82
87
|
else if (arg.startsWith('--secret-guard')) opts.secretGuards.push(...value(arg, argv, () => i++).split(',').map((s) => s.trim()).filter(Boolean));
|
|
83
88
|
else if (arg.startsWith('--access-guard')) opts.accessGuards.push(...value(arg, argv, () => i++).split(',').map((s) => s.trim()).filter(Boolean));
|
|
@@ -152,6 +157,7 @@ async function main() {
|
|
|
152
157
|
|
|
153
158
|
if (!FORMATS.includes(opts.format)) fail(`unknown format "${opts.format}" (expected one of: ${FORMATS.join(', ')})`);
|
|
154
159
|
if (!['retail', 'classic'].includes(opts.game)) fail(`unknown game "${opts.game}" (expected retail or classic)`);
|
|
160
|
+
if (!PATCHES.includes(opts.patch)) fail(`unknown --patch "${opts.patch}" (expected ${PATCHES.join(' or ')})`);
|
|
155
161
|
if (!['warn', 'error', 'off'].includes(opts.conditional)) {
|
|
156
162
|
fail(`unknown --conditional "${opts.conditional}" (expected warn, error or off)`);
|
|
157
163
|
}
|
|
@@ -175,6 +181,7 @@ async function main() {
|
|
|
175
181
|
try {
|
|
176
182
|
merged = await lintPaths(opts.paths, {
|
|
177
183
|
game: opts.game,
|
|
184
|
+
patch: opts.patch,
|
|
178
185
|
conditional: opts.conditional,
|
|
179
186
|
strict: opts.strict,
|
|
180
187
|
disable: opts.disable,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wow-secret-lint",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Maps where a WoW retail addon touches Blizzard's secret-value API, and gates CI on the one error that is certain.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"data/api-snapshot.json",
|
|
37
37
|
"action.yml",
|
|
38
38
|
"README.md",
|
|
39
|
+
"CHANGELOG.md",
|
|
39
40
|
"LICENSE"
|
|
40
41
|
],
|
|
41
42
|
"engines": {
|
package/src/analyze.mjs
CHANGED
|
@@ -19,6 +19,21 @@ import {
|
|
|
19
19
|
COMBAT_LOG_EVENTS,
|
|
20
20
|
COMBAT_LOG_REPLACEMENT,
|
|
21
21
|
booleanTestSeverity,
|
|
22
|
+
DEFAULT_PATCH,
|
|
23
|
+
AURA_SECRET_APIS,
|
|
24
|
+
IDENTITY_SECRET_APIS,
|
|
25
|
+
SELF_EXEMPT_TOKENS,
|
|
26
|
+
AURA_STATE_IDENTITY_APIS,
|
|
27
|
+
CATEGORY_RULES,
|
|
28
|
+
ITERATORS,
|
|
29
|
+
AURA_SUGGESTION,
|
|
30
|
+
REMOVED_CALLS,
|
|
31
|
+
REMOVED_TEMPLATE,
|
|
32
|
+
REMOVED_TEMPLATE_MESSAGE,
|
|
33
|
+
RENAMED_STRUCT_FIELDS,
|
|
34
|
+
ASPECT_FRAME_TYPES,
|
|
35
|
+
AURA_GROUP_METHODS,
|
|
36
|
+
FORBIDDEN_ASPECT_METHODS,
|
|
22
37
|
} from './rules.mjs';
|
|
23
38
|
|
|
24
39
|
const ARITHMETIC = new Set(['+', '-', '*', '/', '%', '^']);
|
|
@@ -86,6 +101,10 @@ class Analyzer {
|
|
|
86
101
|
this.bindingSeq = 0;
|
|
87
102
|
this.callDepth = 0;
|
|
88
103
|
this.analysedWithTaint = new Set();
|
|
104
|
+
this.patch121 = options.patch !== '12.0';
|
|
105
|
+
this.widgetOf = new Map(); // dotted path -> 'AuraContainer' | 'AuraButton'
|
|
106
|
+
this.tableFields = new Map(); // dotted path -> Map(fieldName -> key node)
|
|
107
|
+
this.guardArgDepth = 0; // > 0 while evaluating the arguments of a guard or scrubber call
|
|
89
108
|
}
|
|
90
109
|
|
|
91
110
|
// ---------------------------------------------------------------- findings
|
|
@@ -100,8 +119,13 @@ class Analyzer {
|
|
|
100
119
|
// Findings that rest on SecretReturns report as warnings unless --strict. Whether those
|
|
101
120
|
// APIs really hand a secret to tainted code on every call is unconfirmed in game, so the
|
|
102
121
|
// default is an inventory of exposure, not a verdict. Rules with no taint behind them
|
|
103
|
-
// (WSL008) are deterministic and keep their severity.
|
|
104
|
-
|
|
122
|
+
// (WSL008, WSL014-017) are deterministic and keep their severity, as do the 12.1 aura
|
|
123
|
+
// and identity rules: the patch notes state the secrecy outright and every player enters
|
|
124
|
+
// the states that activate it.
|
|
125
|
+
const patchBreaking = ruleId === 'WSL012' || ruleId === 'WSL013';
|
|
126
|
+
if (taint && taint.kind === 'secret' && sev === 'error' && !this.options.strict && !patchBreaking) {
|
|
127
|
+
sev = 'warning';
|
|
128
|
+
}
|
|
105
129
|
if (this.options.disable.has(ruleId)) return;
|
|
106
130
|
const p = pos(node);
|
|
107
131
|
const key = `${ruleId}:${p.line}:${p.column}`;
|
|
@@ -123,6 +147,12 @@ class Analyzer {
|
|
|
123
147
|
describe(taint) {
|
|
124
148
|
if (!taint) return 'a secret value';
|
|
125
149
|
const name = taint.label ? `'${taint.label}' ` : '';
|
|
150
|
+
if (taint.category === 'aura') {
|
|
151
|
+
return `${name}derives from ${taint.origin}(), secret in 12.1 while auras are secret (combat, encounters, M+, PvP); ${AURA_SUGGESTION}`;
|
|
152
|
+
}
|
|
153
|
+
if (taint.category === 'identity') {
|
|
154
|
+
return `${name}derives from ${taint.origin}(), whose returns are secret in 12.1 while the unit's identity is secret`;
|
|
155
|
+
}
|
|
126
156
|
if (taint.kind === 'conditional') {
|
|
127
157
|
const why = taint.conditions && taint.conditions.length ? taint.conditions.join(', ') : 'a runtime restriction';
|
|
128
158
|
return `${name}derives from ${taint.origin}() (conditionally secret: ${why})`;
|
|
@@ -156,7 +186,7 @@ class Analyzer {
|
|
|
156
186
|
|
|
157
187
|
// ------------------------------------------------------------------ taint
|
|
158
188
|
|
|
159
|
-
makeTaint({ kind, origin, type, conditions, structure, label, bindingId, container }) {
|
|
189
|
+
makeTaint({ kind, origin, type, conditions, structure, label, bindingId, container, category }) {
|
|
160
190
|
return {
|
|
161
191
|
kind,
|
|
162
192
|
origin,
|
|
@@ -165,11 +195,18 @@ class Analyzer {
|
|
|
165
195
|
structure: structure ?? null,
|
|
166
196
|
// A documented structure is not itself a secret; its unmarked fields are.
|
|
167
197
|
container: container === true,
|
|
198
|
+
// 'aura' or 'identity' reroutes operator findings to WSL012/WSL013.
|
|
199
|
+
category: category ?? null,
|
|
168
200
|
label: label ?? null,
|
|
169
201
|
bindingId: bindingId ?? null,
|
|
170
202
|
};
|
|
171
203
|
}
|
|
172
204
|
|
|
205
|
+
/** Operator findings on a categorised taint report as the 12.1 rule for that category. */
|
|
206
|
+
ruleFor(defaultId, taint) {
|
|
207
|
+
return taint && taint.category ? CATEGORY_RULES[taint.category] : defaultId;
|
|
208
|
+
}
|
|
209
|
+
|
|
173
210
|
/** Resolve the callee of a CallExpression to a documented API name, or null. */
|
|
174
211
|
calleeName(node) {
|
|
175
212
|
const base = node.base;
|
|
@@ -191,6 +228,18 @@ class Analyzer {
|
|
|
191
228
|
/** Taint carried by return position `index` (0-based) of a documented API call. */
|
|
192
229
|
returnTaint(name, entry, index, node) {
|
|
193
230
|
const ret = entry.returns && entry.returns[index] ? entry.returns[index] : null;
|
|
231
|
+
// Aura secrecy is behavioural, so the generated docs carry no marker for it and the
|
|
232
|
+
// 12.1 lists seed it here, ahead of the snapshot's own annotations. All return
|
|
233
|
+
// positions are secret, and no structure is recorded because the value is a full
|
|
234
|
+
// secret: reading a field of it is itself the violation.
|
|
235
|
+
if (this.patch121) {
|
|
236
|
+
if (AURA_SECRET_APIS.has(name)) {
|
|
237
|
+
return this.makeTaint({ kind: 'secret', origin: name, type: ret ? ret.type : null, category: 'aura' });
|
|
238
|
+
}
|
|
239
|
+
if (IDENTITY_SECRET_APIS.has(name) && !selfExempt(name, node)) {
|
|
240
|
+
return this.makeTaint({ kind: 'secret', origin: name, type: ret ? ret.type : null, category: 'identity' });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
194
243
|
if (ret && ret.neverSecret) return null;
|
|
195
244
|
const type = ret ? ret.type : null;
|
|
196
245
|
const structure = type && this.api.structures[type] ? type : null;
|
|
@@ -338,6 +387,14 @@ class Analyzer {
|
|
|
338
387
|
if (recv) paths.push(recv);
|
|
339
388
|
}
|
|
340
389
|
if (!paths.length) return empty;
|
|
390
|
+
// An aura is secret or readable as a unit, so a guard naming one field of a
|
|
391
|
+
// category-tainted value (issecretvalue(aura.name)) vouches for the whole value.
|
|
392
|
+
for (const p of [...paths]) {
|
|
393
|
+
const root = p.split('.')[0];
|
|
394
|
+
if (root === p) continue;
|
|
395
|
+
const t = scope.get(root);
|
|
396
|
+
if (t && t.category) paths.push(root);
|
|
397
|
+
}
|
|
341
398
|
const marks = paths.map((p) => ({ path: p, prefix: g.prefix }));
|
|
342
399
|
return g.safeWhen ? { whenTrue: marks, whenFalse: [] } : { whenTrue: [], whenFalse: marks };
|
|
343
400
|
}
|
|
@@ -544,14 +601,14 @@ class Analyzer {
|
|
|
544
601
|
if (t.type === 'MemberExpression' || t.type === 'IndexExpression') {
|
|
545
602
|
const baseTaint = this.taintOf(t.base, scope);
|
|
546
603
|
if (baseTaint && !baseTaint.container && !baseTaint.structure) {
|
|
547
|
-
this.report('WSL005', t, `indexed assignment on a secret value: ${this.describe(baseTaint)}`, {
|
|
604
|
+
this.report(this.ruleFor('WSL005', baseTaint), t, `indexed assignment on a secret value: ${this.describe(baseTaint)}`, {
|
|
548
605
|
taint: baseTaint,
|
|
549
606
|
});
|
|
550
607
|
}
|
|
551
608
|
if (t.type === 'IndexExpression') {
|
|
552
609
|
const keyTaint = this.taintOf(t.index, scope);
|
|
553
610
|
if (keyTaint) {
|
|
554
|
-
this.report('WSL005', t.index, `secret value used as a table key: ${this.describe(keyTaint)}`, {
|
|
611
|
+
this.report(this.ruleFor('WSL005', keyTaint), t.index, `secret value used as a table key: ${this.describe(keyTaint)}`, {
|
|
555
612
|
taint: keyTaint,
|
|
556
613
|
});
|
|
557
614
|
}
|
|
@@ -595,6 +652,7 @@ class Analyzer {
|
|
|
595
652
|
this.cleared.delete(path);
|
|
596
653
|
}
|
|
597
654
|
}
|
|
655
|
+
if (this.patch121) this.trackPatchState(target, i < inits.length ? inits[i] : null);
|
|
598
656
|
});
|
|
599
657
|
return null;
|
|
600
658
|
}
|
|
@@ -621,10 +679,10 @@ class Analyzer {
|
|
|
621
679
|
const t = lt ?? rt;
|
|
622
680
|
if (!t) return;
|
|
623
681
|
if (ARITHMETIC.has(node.operator)) {
|
|
624
|
-
this.report('WSL001', lt ? node.left : node.right, `arithmetic on a secret value: ${this.describe(t)}`, { taint: t });
|
|
682
|
+
this.report(this.ruleFor('WSL001', t), lt ? node.left : node.right, `arithmetic on a secret value: ${this.describe(t)}`, { taint: t });
|
|
625
683
|
if (lt && rt) this.markReported(rt);
|
|
626
684
|
} else if (RELATIONAL.has(node.operator)) {
|
|
627
|
-
this.report('WSL002', lt ? node.left : node.right, `comparison of a secret value: ${this.describe(t)}`, { taint: t });
|
|
685
|
+
this.report(this.ruleFor('WSL002', t), lt ? node.left : node.right, `comparison of a secret value: ${this.describe(t)}`, { taint: t });
|
|
628
686
|
if (lt && rt) this.markReported(rt);
|
|
629
687
|
} else if (node.operator === '..') {
|
|
630
688
|
// Explicitly allowed by the wiki for string and number secrets.
|
|
@@ -648,9 +706,9 @@ class Analyzer {
|
|
|
648
706
|
const t = this.taintOf(node.argument, scope);
|
|
649
707
|
if (!t) return;
|
|
650
708
|
if (node.operator === '#') {
|
|
651
|
-
this.report('WSL004', node.argument, `length operator (#) on a secret value: ${this.describe(t)}`, { taint: t });
|
|
709
|
+
this.report(this.ruleFor('WSL004', t), node.argument, `length operator (#) on a secret value: ${this.describe(t)}`, { taint: t });
|
|
652
710
|
} else if (node.operator === '-') {
|
|
653
|
-
this.report('WSL001', node.argument, `arithmetic on a secret value: ${this.describe(t)}`, { taint: t });
|
|
711
|
+
this.report(this.ruleFor('WSL001', t), node.argument, `arithmetic on a secret value: ${this.describe(t)}`, { taint: t });
|
|
654
712
|
} else if (node.operator === 'not') {
|
|
655
713
|
this.booleanTest(node.argument, t);
|
|
656
714
|
}
|
|
@@ -662,14 +720,18 @@ class Analyzer {
|
|
|
662
720
|
if (node.type === 'IndexExpression') this.evaluate(node.index, scope);
|
|
663
721
|
const baseTaint = this.taintOf(node.base, scope);
|
|
664
722
|
if (baseTaint && !baseTaint.container && !baseTaint.structure) {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
723
|
+
if (baseTaint.category && this.guardArgDepth > 0) {
|
|
724
|
+
this.markUse(baseTaint, true);
|
|
725
|
+
} else {
|
|
726
|
+
this.report(this.ruleFor('WSL005', baseTaint), node, `indexed access on a secret value: ${this.describe(baseTaint)}`, {
|
|
727
|
+
taint: baseTaint,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
668
730
|
}
|
|
669
731
|
if (node.type === 'IndexExpression') {
|
|
670
732
|
const keyTaint = this.taintOf(node.index, scope);
|
|
671
733
|
if (keyTaint) {
|
|
672
|
-
this.report('WSL005', node.index, `secret value used as a table key: ${this.describe(keyTaint)}`, {
|
|
734
|
+
this.report(this.ruleFor('WSL005', keyTaint), node.index, `secret value used as a table key: ${this.describe(keyTaint)}`, {
|
|
673
735
|
taint: keyTaint,
|
|
674
736
|
});
|
|
675
737
|
}
|
|
@@ -719,7 +781,7 @@ class Analyzer {
|
|
|
719
781
|
const target = baseIsMethod ? node.base.base : node.base;
|
|
720
782
|
const t = baseIsMethod ? null : this.taintOf(target, scope);
|
|
721
783
|
if (t && !t.container) {
|
|
722
|
-
this.report('WSL003', node.base, `call of a secret value as-if it were a function: ${this.describe(t)}`, {
|
|
784
|
+
this.report(this.ruleFor('WSL003', t), node.base, `call of a secret value as-if it were a function: ${this.describe(t)}`, {
|
|
723
785
|
taint: t,
|
|
724
786
|
});
|
|
725
787
|
}
|
|
@@ -727,7 +789,17 @@ class Analyzer {
|
|
|
727
789
|
else if (node.base.type !== 'Identifier') this.evaluate(node.base, scope);
|
|
728
790
|
}
|
|
729
791
|
|
|
792
|
+
// AddAuraGroup and friends hand an AuraButton to their initializeFrame callback, so tag
|
|
793
|
+
// that parameter before the callback body is analysed, and untag it afterwards.
|
|
794
|
+
const buttonParams = this.patch121 ? this.tagInitializeFrameParams(callee, args) : [];
|
|
795
|
+
// `issecretvalue(aura.name)` indexes the aura in order to test it; that read is the
|
|
796
|
+
// guard idiom DBM and BigWigs ship, not a violation.
|
|
797
|
+
const guardCall =
|
|
798
|
+
callee && (this.guardShape(callee) || (!callee.method && SCRUBBERS.has(callee.name)));
|
|
799
|
+
if (guardCall) this.guardArgDepth += 1;
|
|
730
800
|
for (const a of args) this.evaluate(a, scope);
|
|
801
|
+
if (guardCall) this.guardArgDepth -= 1;
|
|
802
|
+
for (const p of buttonParams) this.widgetOf.delete(p);
|
|
731
803
|
|
|
732
804
|
if (!callee) return;
|
|
733
805
|
const name = callee.name;
|
|
@@ -742,8 +814,36 @@ class Analyzer {
|
|
|
742
814
|
}
|
|
743
815
|
}
|
|
744
816
|
|
|
817
|
+
if (this.patch121) {
|
|
818
|
+
if (!callee.method) {
|
|
819
|
+
const removed = REMOVED_CALLS[name];
|
|
820
|
+
if (removed) this.report(removed.ruleId, node, removed.message);
|
|
821
|
+
if (name === 'CreateFrame') this.checkCreateFrameTemplates(args);
|
|
822
|
+
} else {
|
|
823
|
+
const recv = node.base && node.base.base ? this.pathOf(node.base.base) : null;
|
|
824
|
+
const widgetType = recv ? this.widgetOf.get(recv) : null;
|
|
825
|
+
const forbidden = widgetType ? FORBIDDEN_ASPECT_METHODS[widgetType][name] : null;
|
|
826
|
+
if (forbidden) {
|
|
827
|
+
this.report(
|
|
828
|
+
'WSL017',
|
|
829
|
+
node,
|
|
830
|
+
`${forbidden.verb} on an ${widgetType} is disallowed by its ${forbidden.aspect} forbidden aspect: ${recv}:${name}()`
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
745
836
|
const argTaints = args.map((a) => this.taintOf(a, scope));
|
|
746
837
|
|
|
838
|
+
if (this.patch121 && !callee.method && ITERATORS.has(name)) {
|
|
839
|
+
for (let i = 0; i < args.length; i++) {
|
|
840
|
+
const t = argTaints[i];
|
|
841
|
+
if (t && t.category) {
|
|
842
|
+
this.report(CATEGORY_RULES[t.category], args[i], `iteration with ${name}() over a secret value: ${this.describe(t)}`, { taint: t });
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
747
847
|
if (!callee.method && SCRUBBERS.has(name)) {
|
|
748
848
|
for (const t of argTaints) this.markUse(t, true);
|
|
749
849
|
return;
|
|
@@ -762,6 +862,7 @@ class Analyzer {
|
|
|
762
862
|
|
|
763
863
|
const entry = callee.method ? null : this.apiEntry(name);
|
|
764
864
|
if (entry) {
|
|
865
|
+
if (this.patch121) this.checkRenamedStructFields(entry, args);
|
|
765
866
|
const sa = entry.secretArguments;
|
|
766
867
|
for (let i = 0; i < args.length; i++) {
|
|
767
868
|
const t = argTaints[i];
|
|
@@ -788,6 +889,93 @@ class Analyzer {
|
|
|
788
889
|
for (const t of argTaints) this.markUse(t, false);
|
|
789
890
|
}
|
|
790
891
|
|
|
892
|
+
// ------------------------------------------------------------- patch 12.1 checks
|
|
893
|
+
|
|
894
|
+
/** Tag initializeFrame callback parameters as AuraButtons; returns the names to untag. */
|
|
895
|
+
tagInitializeFrameParams(callee, args) {
|
|
896
|
+
if (!callee || !callee.method || !AURA_GROUP_METHODS.has(callee.name)) return [];
|
|
897
|
+
const tagged = [];
|
|
898
|
+
for (const a of args) {
|
|
899
|
+
if (!a || a.type !== 'TableConstructorExpression') continue;
|
|
900
|
+
for (const f of a.fields) {
|
|
901
|
+
if (f.type !== 'TableKeyString' || f.key.name !== 'initializeFrame') continue;
|
|
902
|
+
if (!f.value || f.value.type !== 'FunctionDeclaration') continue;
|
|
903
|
+
const p = f.value.parameters && f.value.parameters[0];
|
|
904
|
+
if (p && p.type === 'Identifier' && !this.widgetOf.has(p.name)) {
|
|
905
|
+
this.widgetOf.set(p.name, 'AuraButton');
|
|
906
|
+
tagged.push(p.name);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return tagged;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/** WSL014: a CreateFrame template list naming the removed SecureAuraHeaderTemplate. */
|
|
914
|
+
checkCreateFrameTemplates(args) {
|
|
915
|
+
for (const a of args) {
|
|
916
|
+
const s = stringValue(a);
|
|
917
|
+
if (!s || !s.includes(REMOVED_TEMPLATE)) continue;
|
|
918
|
+
if (s.split(',').some((part) => part.trim() === REMOVED_TEMPLATE)) {
|
|
919
|
+
this.report('WSL014', a, REMOVED_TEMPLATE_MESSAGE);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* WSL016: a field 12.1 renamed, passed inside a documented options structure. The old
|
|
926
|
+
* name is ignored at runtime with no Lua error, which is why it needs a static check.
|
|
927
|
+
*/
|
|
928
|
+
checkRenamedStructFields(entry, args) {
|
|
929
|
+
const docArgs = entry.args || [];
|
|
930
|
+
for (let i = 0; i < args.length && i < docArgs.length; i++) {
|
|
931
|
+
const struct = docArgs[i] && docArgs[i].type ? this.api.structures[docArgs[i].type] : null;
|
|
932
|
+
if (!struct || !struct.fields) continue;
|
|
933
|
+
const passed =
|
|
934
|
+
args[i].type === 'TableConstructorExpression'
|
|
935
|
+
? constructorFields(args[i])
|
|
936
|
+
: args[i].type === 'Identifier'
|
|
937
|
+
? this.tableFields.get(args[i].name)
|
|
938
|
+
: null;
|
|
939
|
+
if (!passed) continue;
|
|
940
|
+
for (const [oldField, newField] of Object.entries(RENAMED_STRUCT_FIELDS)) {
|
|
941
|
+
if (!Object.prototype.hasOwnProperty.call(struct.fields, newField)) continue;
|
|
942
|
+
const keyNode = passed.get(oldField);
|
|
943
|
+
if (keyNode) {
|
|
944
|
+
this.report(
|
|
945
|
+
'WSL016',
|
|
946
|
+
keyNode,
|
|
947
|
+
`${oldField} was removed from ${docArgs[i].type} in 12.1 and is silently ignored (the cooldown swipe never shows); use ${newField} instead`
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/** Keep the path -> widget-type and path -> table-fields maps in step with assignments. */
|
|
955
|
+
trackPatchState(target, init) {
|
|
956
|
+
const path = target.type === 'Identifier' ? target.name : this.pathOf(target);
|
|
957
|
+
if (!path) return;
|
|
958
|
+
// `args.showCountdownFrame = true` after the constructor still counts as a field.
|
|
959
|
+
if (target.type === 'MemberExpression') {
|
|
960
|
+
const fields = this.tableFields.get(this.pathOf(target.base));
|
|
961
|
+
if (fields) fields.set(target.identifier.name, target);
|
|
962
|
+
}
|
|
963
|
+
this.widgetOf.delete(path);
|
|
964
|
+
this.tableFields.delete(path);
|
|
965
|
+
if (!init) return;
|
|
966
|
+
if (init.type === 'TableConstructorExpression') {
|
|
967
|
+
this.tableFields.set(path, constructorFields(init));
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (init.type === 'CallExpression') {
|
|
971
|
+
const callee = this.calleeName(init);
|
|
972
|
+
if (callee && !callee.method && callee.name === 'CreateFrame') {
|
|
973
|
+
const frameType = stringValue(init.arguments[0]);
|
|
974
|
+
if (frameType && ASPECT_FRAME_TYPES.has(frameType)) this.widgetOf.set(path, frameType);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
791
979
|
crossBoundary(local, name, args, argTaints, node, scope) {
|
|
792
980
|
const tainted = argTaints.map((t, i) => (t ? i : -1)).filter((i) => i >= 0);
|
|
793
981
|
if (!tainted.length) return;
|
|
@@ -898,6 +1086,14 @@ class Analyzer {
|
|
|
898
1086
|
|
|
899
1087
|
booleanTest(node, taint) {
|
|
900
1088
|
if (taint.container) return;
|
|
1089
|
+
// The 12.1 aura APIs return a table or nil, never a boolean, and nil-checking the
|
|
1090
|
+
// return is the sanctioned pattern, so a boolean test on aura taint stays silent.
|
|
1091
|
+
// (The AuraData structure left the generated docs in 12.1, so the type lookup below
|
|
1092
|
+
// cannot vouch for it any more.)
|
|
1093
|
+
if (taint.category === 'aura') {
|
|
1094
|
+
this.markUse(taint, true);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
901
1097
|
const sev = booleanTestSeverity(taint.type, this.api.structures);
|
|
902
1098
|
if (!sev) {
|
|
903
1099
|
this.markUse(taint, true);
|
|
@@ -907,7 +1103,7 @@ class Analyzer {
|
|
|
907
1103
|
sev === 'error'
|
|
908
1104
|
? `its documented return type is bool`
|
|
909
1105
|
: `its documented return type is unknown, so this may be a boolean secret`;
|
|
910
|
-
this.report('WSL007', node, `boolean test on a secret value (${why}): ${this.describe(taint)}`, {
|
|
1106
|
+
this.report(this.ruleFor('WSL007', taint), node, `boolean test on a secret value (${why}): ${this.describe(taint)}`, {
|
|
911
1107
|
severity: sev,
|
|
912
1108
|
taint,
|
|
913
1109
|
});
|
|
@@ -970,6 +1166,27 @@ function exits(body) {
|
|
|
970
1166
|
return !!last && (last.type === 'ReturnStatement' || last.type === 'BreakStatement');
|
|
971
1167
|
}
|
|
972
1168
|
|
|
1169
|
+
/** True when a literal unit token keeps this identity API non-secret (see rules.mjs). */
|
|
1170
|
+
function selfExempt(name, node) {
|
|
1171
|
+
const token = node && Array.isArray(node.arguments) ? stringValue(node.arguments[0]) : null;
|
|
1172
|
+
if (token === null) return false;
|
|
1173
|
+
if (AURA_STATE_IDENTITY_APIS.has(name)) return SELF_EXEMPT_TOKENS.has(token);
|
|
1174
|
+
return token === 'player';
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/** String-keyed fields of a table constructor, mapped to their key nodes. */
|
|
1178
|
+
function constructorFields(tableNode) {
|
|
1179
|
+
const fields = new Map();
|
|
1180
|
+
for (const f of tableNode.fields) {
|
|
1181
|
+
if (f.type === 'TableKeyString') fields.set(f.key.name, f.key);
|
|
1182
|
+
else if (f.type === 'TableKey') {
|
|
1183
|
+
const k = stringValue(f.key);
|
|
1184
|
+
if (k !== null) fields.set(k, f.key);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return fields;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
973
1190
|
/**
|
|
974
1191
|
* Analyse one Lua source file.
|
|
975
1192
|
* @returns {{ findings: object[], parseError: object|null }}
|
|
@@ -981,6 +1198,7 @@ export function analyzeSource(source, filePath, api, options = {}) {
|
|
|
981
1198
|
secretGuards: new Set(options.secretGuards ?? []),
|
|
982
1199
|
accessGuards: new Set(options.accessGuards ?? []),
|
|
983
1200
|
strict: options.strict === true,
|
|
1201
|
+
patch: options.patch ?? DEFAULT_PATCH,
|
|
984
1202
|
};
|
|
985
1203
|
const parseOptions = { locations: true, ranges: false, comments: false, scope: false };
|
|
986
1204
|
let ast;
|
package/src/index.mjs
CHANGED
|
@@ -5,14 +5,15 @@ import { relative, resolve, dirname } from 'node:path';
|
|
|
5
5
|
import { analyzeSource } from './analyze.mjs';
|
|
6
6
|
import { loadSnapshot } from './apidata.mjs';
|
|
7
7
|
import { findTocFilesDeep, parseToc, resolveTocFiles, collectLuaFiles, isRetailToc, toPosix } from './toc.mjs';
|
|
8
|
+
import { DEFAULT_PATCH } from './rules.mjs';
|
|
8
9
|
|
|
9
|
-
export { RULES, RULE_IDS } from './rules.mjs';
|
|
10
|
+
export { RULES, RULE_IDS, PATCHES, DEFAULT_PATCH } from './rules.mjs';
|
|
10
11
|
export { analyzeSource } from './analyze.mjs';
|
|
11
12
|
export { loadSnapshot, refreshSnapshot, writeSnapshot, extractFile, buildIndex, SNAPSHOT_PATH } from './apidata.mjs';
|
|
12
13
|
export { format, formatStylish, formatJson, formatGithub, FORMATS } from './report.mjs';
|
|
13
14
|
export { parseToc, findTocFiles, findTocFilesDeep, isRetailToc } from './toc.mjs';
|
|
14
15
|
|
|
15
|
-
export const VERSION = '1.
|
|
16
|
+
export const VERSION = '1.3.0';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Lint an addon directory or a single .lua/.toc file.
|
|
@@ -20,6 +21,7 @@ export const VERSION = '1.2.0';
|
|
|
20
21
|
* @param {string} target path to an addon folder, a .toc, or a .lua file
|
|
21
22
|
* @param {object} options
|
|
22
23
|
* @param {'retail'|'classic'} options.game
|
|
24
|
+
* @param {'12.0'|'12.1'} options.patch which patch surface the built-in rules check (default 12.1)
|
|
23
25
|
* @param {'warn'|'error'|'off'} options.conditional how to treat conditionally secret APIs
|
|
24
26
|
* @param {string[]} options.disable rule ids to silence
|
|
25
27
|
* @param {string[]} options.secretGuards extra is-secret wrapper names, e.g. IsSecret
|
|
@@ -35,6 +37,7 @@ export async function lint(target, options = {}) {
|
|
|
35
37
|
version: VERSION,
|
|
36
38
|
target,
|
|
37
39
|
game,
|
|
40
|
+
patch: options.patch ?? DEFAULT_PATCH,
|
|
38
41
|
filesScanned: 0,
|
|
39
42
|
findings: [],
|
|
40
43
|
parseErrors: [],
|
|
@@ -121,6 +124,7 @@ export async function lint(target, options = {}) {
|
|
|
121
124
|
secretGuards: new Set(options.secretGuards ?? []),
|
|
122
125
|
accessGuards: new Set(options.accessGuards ?? []),
|
|
123
126
|
strict: options.strict === true,
|
|
127
|
+
patch: options.patch,
|
|
124
128
|
};
|
|
125
129
|
|
|
126
130
|
for (const file of files) {
|
|
@@ -152,6 +156,7 @@ export function byLocation(a, b) {
|
|
|
152
156
|
export async function lintPaths(paths, options = {}) {
|
|
153
157
|
const merged = {
|
|
154
158
|
version: VERSION,
|
|
159
|
+
patch: options.patch ?? DEFAULT_PATCH,
|
|
155
160
|
filesScanned: 0,
|
|
156
161
|
findings: [],
|
|
157
162
|
parseErrors: [],
|
package/src/report.mjs
CHANGED
package/src/rules.mjs
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
const WIKI = 'https://warcraft.wiki.gg/wiki/Secret_Values';
|
|
5
5
|
const PATCH = 'https://warcraft.wiki.gg/wiki/Patch_12.0.0/API_changes';
|
|
6
|
+
const PATCH121 = 'https://warcraft.wiki.gg/wiki/Patch_12.1.0/API_changes';
|
|
7
|
+
|
|
8
|
+
export const PATCHES = ['12.0', '12.1'];
|
|
9
|
+
export const DEFAULT_PATCH = '12.1';
|
|
6
10
|
|
|
7
11
|
export const RULES = {
|
|
8
12
|
WSL001: {
|
|
@@ -60,6 +64,42 @@ export const RULES = {
|
|
|
60
64
|
summary: 'tostring() on a secret value',
|
|
61
65
|
source: `Inferred. tostring is absent from the allowed list on ${WIKI} and is not documented as forbidden either; kept a warning until confirmed in game.`,
|
|
62
66
|
},
|
|
67
|
+
WSL012: {
|
|
68
|
+
severity: 'error',
|
|
69
|
+
patch: '12.1',
|
|
70
|
+
summary: 'forbidden operation on a secret aura vector',
|
|
71
|
+
source: `${PATCH121} : "when auras are secret (during combat, encounters, M+, and PvP matches), all of the UnitAura APIs will now either return full secrets or nil when called by addons. That means that APIs like GetUnitAuras and GetUnitAuraInstanceIDs will return a secret vector, meaning addon code will not be able to determine how many auras it contains or iterate through it for display."`,
|
|
72
|
+
},
|
|
73
|
+
WSL013: {
|
|
74
|
+
severity: 'error',
|
|
75
|
+
patch: '12.1',
|
|
76
|
+
summary: 'forbidden operation on a secret unit identity value',
|
|
77
|
+
source: `${PATCH121} : "A number of Unit APIs have been changed to return secret values when the unit's identity is secret", naming UnitClass, UnitClassBase, UnitRace, UnitSex and UnitSexBase; "UnitIsCharmed and UnitIsPossessed APIs now return secret values when auras are secret."`,
|
|
78
|
+
},
|
|
79
|
+
WSL014: {
|
|
80
|
+
severity: 'error',
|
|
81
|
+
patch: '12.1',
|
|
82
|
+
summary: 'symbol removed or renamed in 12.1',
|
|
83
|
+
source: `${PATCH121} : "UIParentLoadAddOn has been renamed to LoadAddOnWithErrorHandling." / "CanAccessObject has been replaced with FrameScriptObject:CanBeAccessedInContext." / "SecureAuraHeaderTemplate has been removed from Mainline"; C_UnitAuras.TriggerPrivateAuraShowDispelType is in the Removed list.`,
|
|
84
|
+
},
|
|
85
|
+
WSL015: {
|
|
86
|
+
severity: 'warning',
|
|
87
|
+
patch: '12.1',
|
|
88
|
+
summary: 'deprecated getglobal/setglobal',
|
|
89
|
+
source: `${PATCH121} : "Deprecated getglobal and setglobal."`,
|
|
90
|
+
},
|
|
91
|
+
WSL016: {
|
|
92
|
+
severity: 'warning',
|
|
93
|
+
patch: '12.1',
|
|
94
|
+
summary: 'showCountdownFrame passed to a private-aura API',
|
|
95
|
+
source: `${PATCH121} : showCountdownFrame was removed from AddPrivateAuraAnchorArgs and UnitPrivateAuraAnchorInfo, replaced by showCooldownFrame, showCooldownEdge and showDispelIcon. The field is silently ignored, so the cooldown swipe stops appearing with no Lua error.`,
|
|
96
|
+
},
|
|
97
|
+
WSL017: {
|
|
98
|
+
severity: 'error',
|
|
99
|
+
patch: '12.1',
|
|
100
|
+
summary: 'forbidden-aspect operation on an AuraButton or AuraContainer',
|
|
101
|
+
source: `${PATCH121} : "Aura Buttons have had the following Forbidden Aspects applied to them: UntrustedScriptExecution, UntrustedLayoutScriptExecution, AlwaysPropagateInput, ScriptedInput, and QueryFocus" / "Aura Containers have had the EventRegistrations Forbidden Aspect applied to them."`,
|
|
102
|
+
},
|
|
63
103
|
};
|
|
64
104
|
|
|
65
105
|
export const RULE_IDS = Object.keys(RULES);
|
|
@@ -137,6 +177,143 @@ const NON_BOOL_TYPES = new Set([
|
|
|
137
177
|
'textureKit',
|
|
138
178
|
]);
|
|
139
179
|
|
|
180
|
+
// ------------------------------------------------------------- patch 12.1 surface
|
|
181
|
+
//
|
|
182
|
+
// None of this is in Blizzard's generated documentation. Aura secrecy is behavioural
|
|
183
|
+
// ("when auras are secret ... return full secrets or nil"), documented only in the 12.1
|
|
184
|
+
// patch notes, so the snapshot cannot carry it and these lists seed the taint instead.
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* APIs whose returns are a secret aura vector or secret aura data in 12.1. Taints they
|
|
188
|
+
* produce carry category 'aura' and report as WSL012. Bare names are listed too because
|
|
189
|
+
* the snapshot registers global aliases for namespaced functions.
|
|
190
|
+
*/
|
|
191
|
+
export const AURA_SECRET_APIS = new Set(
|
|
192
|
+
[
|
|
193
|
+
'C_UnitAuras.GetUnitAuras',
|
|
194
|
+
'C_UnitAuras.GetUnitAuraInstanceIDs',
|
|
195
|
+
'C_UnitAuras.GetAuraSlots',
|
|
196
|
+
'C_UnitAuras.GetAuraDataByIndex',
|
|
197
|
+
'C_UnitAuras.GetAuraDataBySlot',
|
|
198
|
+
'C_UnitAuras.GetAuraDataByAuraInstanceID',
|
|
199
|
+
'C_UnitAuras.GetBuffDataByIndex',
|
|
200
|
+
'C_UnitAuras.GetDebuffDataByIndex',
|
|
201
|
+
// "all of the UnitAura APIs" per the 12.1 notes, so the by-spell lookups too.
|
|
202
|
+
'C_UnitAuras.GetAuraDataBySpellName',
|
|
203
|
+
'C_UnitAuras.GetUnitAuraBySpellID',
|
|
204
|
+
'C_UnitAuras.GetPlayerAuraBySpellID',
|
|
205
|
+
'C_UnitAuras.GetCooldownAuraBySpellID',
|
|
206
|
+
].flatMap((n) => [n, n.slice(n.indexOf('.') + 1)])
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
/** Unit APIs the 12.1 notes name as returning secrets. Category 'identity', rule WSL013. */
|
|
210
|
+
export const IDENTITY_SECRET_APIS = new Set([
|
|
211
|
+
'UnitClass',
|
|
212
|
+
'UnitClassBase',
|
|
213
|
+
'UnitRace',
|
|
214
|
+
'UnitSex',
|
|
215
|
+
'UnitSexBase',
|
|
216
|
+
'UnitIsCharmed',
|
|
217
|
+
'UnitIsPossessed',
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Literal unit tokens that keep identity APIs non-secret. The 12.1 notes state it for
|
|
222
|
+
* UnitIsCharmed/UnitIsPossessed ("no longer return secret values if the unit token passed
|
|
223
|
+
* is 'player', 'pet', or 'vehicle'"); for the rest only "player" is exempted, because the
|
|
224
|
+
* change exists to stop addons comparing secret units and a unit is never secret to itself.
|
|
225
|
+
*/
|
|
226
|
+
export const SELF_EXEMPT_TOKENS = new Set(['player', 'pet', 'vehicle']);
|
|
227
|
+
export const AURA_STATE_IDENTITY_APIS = new Set(['UnitIsCharmed', 'UnitIsPossessed']);
|
|
228
|
+
|
|
229
|
+
/** Operator findings on a categorised taint report as the 12.1 rule instead. */
|
|
230
|
+
export const CATEGORY_RULES = { aura: 'WSL012', identity: 'WSL013' };
|
|
231
|
+
|
|
232
|
+
/** Iterating a secret vector cannot work; flagged for categorised taints only. */
|
|
233
|
+
export const ITERATORS = new Set(['pairs', 'ipairs', 'next', 'unpack']);
|
|
234
|
+
|
|
235
|
+
export const AURA_SUGGESTION =
|
|
236
|
+
'show auras with an AuraContainer (AddAuraGroup/AddAuraSlot) instead';
|
|
237
|
+
|
|
238
|
+
/** Calls removed, renamed or deprecated in 12.1, with the replacement each message names. */
|
|
239
|
+
export const REMOVED_CALLS = {
|
|
240
|
+
UIParentLoadAddOn: {
|
|
241
|
+
ruleId: 'WSL014',
|
|
242
|
+
message: 'UIParentLoadAddOn() was renamed in 12.1; call LoadAddOnWithErrorHandling() instead',
|
|
243
|
+
},
|
|
244
|
+
CanAccessObject: {
|
|
245
|
+
ruleId: 'WSL014',
|
|
246
|
+
message: 'CanAccessObject() was removed in 12.1; use FrameScriptObject:CanBeAccessedInContext() instead',
|
|
247
|
+
},
|
|
248
|
+
'C_UnitAuras.TriggerPrivateAuraShowDispelType': {
|
|
249
|
+
ruleId: 'WSL014',
|
|
250
|
+
message:
|
|
251
|
+
'C_UnitAuras.TriggerPrivateAuraShowDispelType() was removed in 12.1; use the showDispelIcon option on private aura anchors instead',
|
|
252
|
+
},
|
|
253
|
+
getglobal: {
|
|
254
|
+
ruleId: 'WSL015',
|
|
255
|
+
message: 'getglobal() is deprecated in 12.1; use _G[name] instead',
|
|
256
|
+
},
|
|
257
|
+
setglobal: {
|
|
258
|
+
ruleId: 'WSL015',
|
|
259
|
+
message: 'setglobal() is deprecated in 12.1; use _G[name] = value instead',
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export const REMOVED_TEMPLATE = 'SecureAuraHeaderTemplate';
|
|
264
|
+
export const REMOVED_TEMPLATE_MESSAGE =
|
|
265
|
+
'SecureAuraHeaderTemplate was removed from Mainline in 12.1; migrate to an AuraContainer (AddAuraGroup/AddAuraSlot)';
|
|
266
|
+
|
|
267
|
+
/** Structure fields renamed in 12.1 whose old name is silently ignored at runtime. */
|
|
268
|
+
export const RENAMED_STRUCT_FIELDS = {
|
|
269
|
+
showCountdownFrame: 'showCooldownFrame',
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/** CreateFrame frame types that carry Forbidden Aspects from creation. */
|
|
273
|
+
export const ASPECT_FRAME_TYPES = new Set(['AuraContainer', 'AuraButton']);
|
|
274
|
+
|
|
275
|
+
/** Container methods whose options table hands AuraButtons to an initializeFrame callback. */
|
|
276
|
+
export const AURA_GROUP_METHODS = new Set(['AddAuraGroup', 'AddAuraSlot', 'AddItemEnchantment']);
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Methods disallowed by the Forbidden Aspects each frame type carries. Buttons additionally
|
|
280
|
+
* get event registration flagged: they join the container's forbidden partition and the 12.1
|
|
281
|
+
* announcement says addons cannot "register events on those buttons".
|
|
282
|
+
*/
|
|
283
|
+
const BUTTON_SCRIPTS = { aspect: 'UntrustedScriptExecution', verb: 'installing a script handler' };
|
|
284
|
+
const EVENT_REG = { aspect: 'EventRegistrations', verb: 'registering for events' };
|
|
285
|
+
const INPUT_CALL = { aspect: 'ScriptedInput', verb: 'calling an input API' };
|
|
286
|
+
const FOCUS_QUERY = { aspect: 'QueryFocus', verb: 'querying focus' };
|
|
287
|
+
|
|
288
|
+
export const FORBIDDEN_ASPECT_METHODS = {
|
|
289
|
+
AuraButton: {
|
|
290
|
+
SetScript: BUTTON_SCRIPTS,
|
|
291
|
+
HookScript: BUTTON_SCRIPTS,
|
|
292
|
+
RegisterEvent: EVENT_REG,
|
|
293
|
+
RegisterUnitEvent: EVENT_REG,
|
|
294
|
+
RegisterAllEvents: EVENT_REG,
|
|
295
|
+
Click: INPUT_CALL,
|
|
296
|
+
SetFocus: INPUT_CALL,
|
|
297
|
+
EnableMouse: INPUT_CALL,
|
|
298
|
+
EnableKeyboard: INPUT_CALL,
|
|
299
|
+
EnableMouseWheel: INPUT_CALL,
|
|
300
|
+
EnableGamePadButton: INPUT_CALL,
|
|
301
|
+
EnableGamePadStick: INPUT_CALL,
|
|
302
|
+
SetPropagateKeyboardInput: INPUT_CALL,
|
|
303
|
+
SetPassThroughButtons: INPUT_CALL,
|
|
304
|
+
RegisterForClicks: INPUT_CALL,
|
|
305
|
+
RegisterForDrag: INPUT_CALL,
|
|
306
|
+
IsMouseOver: FOCUS_QUERY,
|
|
307
|
+
IsMouseMotionFocus: FOCUS_QUERY,
|
|
308
|
+
HasFocus: FOCUS_QUERY,
|
|
309
|
+
},
|
|
310
|
+
AuraContainer: {
|
|
311
|
+
RegisterEvent: EVENT_REG,
|
|
312
|
+
RegisterUnitEvent: EVENT_REG,
|
|
313
|
+
RegisterAllEvents: EVENT_REG,
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
140
317
|
/**
|
|
141
318
|
* Decide how a boolean test on a secret should be reported.
|
|
142
319
|
* Returns 'error' when the documented type is bool, null when it is a documented non-bool
|