penthera 1.2.0 → 1.5.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.
Files changed (101) hide show
  1. package/CHANGELOG.md +269 -0
  2. package/README.md +493 -45
  3. package/docs/METHODOLOGY.md +321 -0
  4. package/docs/compliance-cra.md +141 -0
  5. package/docs/compliance-nis2.md +3 -1
  6. package/docs/evidence.md +158 -0
  7. package/docs/examples/github-actions-deps.yml +105 -0
  8. package/docs/examples/github-actions-scan.yml +1 -1
  9. package/docs/examples/pr-dependency-review.yml +56 -0
  10. package/docs/funding/due-diligence-checklist.md +79 -0
  11. package/docs/funding/nlnet-ngi-zero-draft.md +273 -0
  12. package/docs/funding/partner-one-pager.md +84 -0
  13. package/docs/funding/sponsor-tiers.md +81 -0
  14. package/docs/index.md +12 -1
  15. package/docs/local-loop.md +276 -0
  16. package/docs/plan.md +100 -0
  17. package/docs/references.bib +539 -0
  18. package/docs/references.md +77 -0
  19. package/docs/release-plan.md +228 -0
  20. package/lib/adaptive-probes.js +8 -5
  21. package/lib/agents/discover.js +384 -0
  22. package/lib/agents/hidden-content.js +387 -0
  23. package/lib/agents/index.js +286 -0
  24. package/lib/agents/lock.js +115 -0
  25. package/lib/agents/owasp.js +83 -0
  26. package/lib/agents/rules.js +376 -0
  27. package/lib/compliance/cra.js +525 -0
  28. package/lib/crawler.js +5 -15
  29. package/lib/data/retire-jsrepository.json.gz +0 -0
  30. package/lib/data/retire-jsrepository.meta.json +7 -0
  31. package/lib/deps/capabilities.js +523 -0
  32. package/lib/deps/diff.js +84 -0
  33. package/lib/deps/index.js +440 -0
  34. package/lib/deps/license.js +329 -0
  35. package/lib/deps/lockfile.js +109 -0
  36. package/lib/deps/online.js +132 -0
  37. package/lib/deps/popular-packages.json +1 -0
  38. package/lib/deps/report.js +190 -0
  39. package/lib/deps/source.js +171 -0
  40. package/lib/deps/spdx.json +1 -0
  41. package/lib/deps/typosquat.js +113 -0
  42. package/lib/fix/apply.js +95 -0
  43. package/lib/fix/diff.js +199 -0
  44. package/lib/fix/index.js +129 -0
  45. package/lib/fix/model.js +108 -0
  46. package/lib/fix/recipes/cookies.js +130 -0
  47. package/lib/fix/recipes/cors.js +116 -0
  48. package/lib/fix/recipes/headers.js +250 -0
  49. package/lib/fix/recipes/rate-limit.js +211 -0
  50. package/lib/fix/recipes/secrets.js +110 -0
  51. package/lib/fix/util.js +231 -0
  52. package/lib/fuzzer.js +14 -8
  53. package/lib/hook/index.js +312 -0
  54. package/lib/hook/policy.js +62 -0
  55. package/lib/injections.js +19 -15
  56. package/lib/install/index.js +271 -0
  57. package/lib/install/policy.js +125 -0
  58. package/lib/jurisdictions/nl.json +9 -0
  59. package/lib/memory/history.js +52 -0
  60. package/lib/memory/index.js +111 -0
  61. package/lib/memory/repo-suppress.js +64 -0
  62. package/lib/memory/store.js +325 -0
  63. package/lib/memory/views.js +198 -0
  64. package/lib/params.js +4 -12
  65. package/lib/provenance.js +2 -0
  66. package/lib/recon.js +12 -20
  67. package/lib/references.js +31 -0
  68. package/lib/references.json +374 -0
  69. package/lib/remediation/index.js +28 -0
  70. package/lib/retirejs.js +50 -22
  71. package/lib/templates.js +7 -10
  72. package/lib/whitebox/aibom.js +384 -0
  73. package/lib/whitebox/frameworks.js +1 -1
  74. package/lib/whitebox/sbom.js +45 -10
  75. package/lib/whitebox/secrets.js +11 -1
  76. package/package.json +17 -4
  77. package/skills/penthera/SKILL.md +132 -22
  78. package/src/agent/git.js +87 -0
  79. package/src/agent/loop.js +519 -0
  80. package/src/agent/tests.js +48 -0
  81. package/src/cli/agent.js +183 -0
  82. package/src/cli/deps.js +104 -0
  83. package/src/cli/evidence.js +67 -0
  84. package/src/cli/fix.js +191 -0
  85. package/src/cli/hook.js +131 -0
  86. package/src/cli/install.js +225 -0
  87. package/src/cli/memory.js +146 -0
  88. package/src/cli/merge-results.js +2 -0
  89. package/src/cli/report-outputs.js +72 -0
  90. package/src/cli/run-scan.js +2 -0
  91. package/src/cli.js +212 -8
  92. package/src/report/audience.js +270 -0
  93. package/src/report/evidence.js +253 -0
  94. package/src/report/markdown.js +65 -2
  95. package/src/report/model.js +212 -0
  96. package/src/report/template.js +175 -0
  97. package/src/report/timeline.js +145 -0
  98. package/src/reporter.js +109 -5
  99. package/src/scan-url.js +31 -7
  100. package/src/utils/http.js +252 -11
  101. package/src/utils/url.js +23 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,275 @@ All notable changes to Penthera are documented here. Versioning follows [SemVer]
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.5.0] - 2026-10-19
8
+
9
+ Blocks problems at commit time, adds license policy, and extends the EU
10
+ evidence to the Cyber Resilience Act. The GitHub Action is now ready for the
11
+ Marketplace as "Penthera Security Review". Penthera also has a clearer
12
+ problem statement: open-source security for the apps and AI systems you ship.
13
+
14
+ ### Docs
15
+
16
+ - Problem statement: "open-source security for the apps and AI systems you
17
+ ship", organised around three moments (before you deploy, what you build
18
+ with, after you ship). README, package, plugin, skill and repository
19
+ descriptions updated. Roadmap gains v1.6.0 "Before you deploy" (preview
20
+ deployment scanning, `penthera ship`) and v1.7.0 "After you ship"
21
+ (`penthera watch`). New `docs/plan.md` sets measurable quality gates to v2.0.
22
+
23
+ ### Added
24
+
25
+ - **`penthera hook install|uninstall|run`**: a pre-commit hook that checks
26
+ staged content only (the git index): secrets and AI provider keys, the
27
+ `penthera agents` rules on staged `.mcp.json`, `CLAUDE.md`, `AGENTS.md`,
28
+ `.cursor/`, `.claude/` and `skills/`, and, when `package-lock.json` is staged,
29
+ the capability diff against HEAD plus the license policy for the packages the
30
+ commit adds. Blocks on high and critical by default; one line per finding with
31
+ a fix hint and a `penthera.suppress.json` id. An existing hook is kept as
32
+ `pre-commit.pre-penthera`, runs first, and is restored by `uninstall`. Refuses
33
+ to write into a `core.hooksPath` managed by husky or lefthook without `--force`.
34
+ `.pre-commit-hooks.yaml` for pre-commit.com. Configured by a new `hook`
35
+ section in `penthera.policy.json` (`blockOn`, `checks`, `ignore`)
36
+ - **License policy**: `penthera deps --license-policy <preset|file>` and a
37
+ `licenses` section in `penthera.policy.json` (`preset`, `allow`, `deny`,
38
+ `allowPackages`, `unknown`). Presets `permissive`, `weak-copyleft-ok` and
39
+ `no-agpl`. A small SPDX expression parser handles `AND`/`OR`/`WITH`,
40
+ parentheses, `+` and `LicenseRef-`, and matches deprecated ids (`GPL-3.0`) to
41
+ their current form. Breaches and `UNLICENSED` are high; missing, custom and
42
+ non-SPDX licenses are medium. Findings use category `license`; the SPDX id
43
+ list is vendored (`lib/deps/spdx.json`). CycloneDX SBOM components now carry
44
+ their license
45
+ - **CRA Article 14 reporting helper** (`penthera --cra-init cra.json`,
46
+ `penthera --cra cra.json`, `lib/compliance/cra.js`): for an actively exploited
47
+ vulnerability (24h early warning, 72h notification, final report 14 days after
48
+ a fix is available) or a severe incident (24h, 72h, final report one month
49
+ after the notification). Computes due dates in UTC and local time (hours
50
+ elapsed, days and months on the calendar, DST-safe), flags overdue and late
51
+ reports, drafts each report against Art. 14(2)/(4) with `NEEDS INPUT` for
52
+ missing facts, adds an Art. 14(8) user advisory, and can prefill the affected
53
+ component, purl and CVEs from a Penthera SBOM and scan JSON. NL profile names
54
+ NCSC-NL and the ENISA Single Reporting Platform. Drafts, not the official
55
+ form, not legal advice. Guide: `docs/compliance-cra.md`
56
+ - **CycloneDX 1.6 AI-BOM** (`penthera --repo . --aibom aibom.cdx.json`,
57
+ `lib/whitebox/aibom.js`): AI SDK dependencies, model identifiers in code as
58
+ `machine-learning-model` components with `modelCard` and file:line evidence,
59
+ MCP servers as `services`, and Agent Skills and prompt files as hashed `data`
60
+ components. Offline. Tests validate the output against the official
61
+ CycloneDX 1.6 schema, vendored in `tests/fixtures/cyclonedx/` (new dev
62
+ dependency: `ajv`)
63
+
64
+ ## [1.4.0] - 2026-10-19
65
+
66
+ The biggest release so far, covering the v1.3.0 "AI toolchain" and v1.4.0
67
+ "Local loop" milestones together. Penthera now does the routine work of a small
68
+ security team on your own machine:
69
+ - tests your app;
70
+ - vets and guards your dependencies;
71
+ - audits your AI tooling;
72
+ - fixes what it finds on a local branch and verifies each fix;
73
+ - writes reports and evidence for auditors.
74
+
75
+ It still runs offline, with no telemetry. The first outside contribution, from
76
+ @Nurana100, is in this release.
77
+
78
+ Requires Node 22+. Upgrading from 1.1.x or 1.2.x needs no config changes.
79
+
80
+ ### Added
81
+
82
+ - **GitHub Action** (`uses: danoszz/penthera@<sha>`, `action.yml`): on PRs that
83
+ change the lockfile it runs `penthera deps --base <PR base>`, upserts one PR
84
+ comment marked `<!-- penthera-deps -->` (edited in place on every push), and
85
+ uploads SARIF. Runs from the action's own checkout with no npx or npm install.
86
+ Fork PRs get the diff in the job summary. Also `mode: agents` and `mode: scan`,
87
+ plus `fail-on`. Example: `docs/examples/pr-dependency-review.yml`
88
+ - **`penthera agents [path] [--home] [--pin]`**: an offline, deterministic audit
89
+ of AI developer tooling. It finds and parses `.mcp.json`, `.cursor/`,
90
+ `.claude/`, `.vscode/mcp.json`, `AGENTS.md`, `CLAUDE.md`, `.cursorrules`,
91
+ `.github/copilot-instructions.md` and skill folders; with `--home` also
92
+ `~/.claude` (settings, hooks, permissions, plugins, skills), `~/.claude.json`,
93
+ `~/.cursor/mcp.json` and the Claude Desktop config. Read-only, no network, no
94
+ LLM judge
95
+ - Hidden-content engine (`lib/agents/hidden-content.js`): Unicode tag
96
+ characters (decoded), zero-width and bidi controls, variation-selector
97
+ smuggling, whitespace inflation, instructions in HTML comments, text aimed at
98
+ a scanner, base64 blobs (decoded and classified), binaries and archives in
99
+ skill folders by magic bytes, symlinks that leave a skill, and `curl | sh` /
100
+ `eval` style remote execution
101
+ - Risky-config rules: unpinned `npx -y` / `uvx` / `bunx` / `pnpm dlx` MCP
102
+ servers, inline secrets in server env/headers/args (via `findSecretsInText()`),
103
+ plain-HTTP remote servers, hooks that auto-approve or pipe to a shell,
104
+ `Bash(*)` and other wildcard permissions, `bypassPermissions`, credential-file
105
+ allows, wildcard `allowed-tools`, and unpinned plugin marketplaces
106
+ - `penthera.lock`: `--pin` records SHA-256 hashes of every skill folder, MCP
107
+ server definition, hook and installed plugin; later runs report changed (rug
108
+ pull), new and removed entries
109
+ - A–F grade for the agent setup in the terminal and Markdown reports. Findings
110
+ use category `agent-config`, map to OWASP MCP Top 10 and Agentic (ASI) ids,
111
+ and get per-rule SARIF ids (`agents/<rule>`)
112
+ - Agent Skill: Workflow 6 ("audit my MCP servers / skills / agent config")
113
+ - `docs/METHODOLOGY.md`: threat model, what black-box and white-box scanning
114
+ can and cannot find, the confidence model, false-positive controls, the
115
+ non-destructive default, offline operation, and how new attack techniques
116
+ are adopted (reviewed rules in releases, never fetched at run time). Design
117
+ choices cite published research and standards
118
+ - `docs/references.bib` and `docs/references.md`: 48 references, each checked
119
+ against the publisher or standards body, grouped by topic
120
+ - Per-finding citations: `lib/references.json` maps each finding category to
121
+ 1-3 references. The Markdown report adds a "Why this matters / Sources" line
122
+ per finding and a "Sources" line per action-plan item; SARIF rules get
123
+ `helpUri`, `help` and `properties.references`
124
+ - Accuracy benchmark: `npm run bench` scans the mock server, two new local lab
125
+ targets (vulnerable and decoy) and a generated repo fixture, scores precision
126
+ and recall against `bench/ground-truth.json`, and writes `bench/RESULTS.md`.
127
+ Offline and seeded; `npm test` fails if it drops below the CI floor.
128
+ Optional Juice Shop and crAPI targets are documented
129
+ - `CITATION.cff`, so GitHub shows "Cite this repository"
130
+ - **`penthera deps [path]`**: offline, deterministic dependency risk scanning
131
+ for npm. Reads `package-lock.json` (v1-v3), statically scans every installed
132
+ package and reports what it can do, with `file:line` evidence: install
133
+ scripts (including implicit `node-gyp`), shell, network, writes outside its
134
+ folder, environment reads and bulk access, `eval` / `new Function` / `vm`,
135
+ computed `require`/`import`, native binaries, and obfuscation signals
136
+ - **Capability diff** (`--diff <lockfile|dir>`, `--base <git-ref>`): packages
137
+ that are new or that gained capabilities in an upgrade. Old versions come
138
+ from the base tree or npm's local content cache, verified against the
139
+ lockfile integrity, so the diff works offline
140
+ - Attack indicators: writes to `~/.claude`, `~/.cursor`, `.mcp.json` or the
141
+ Claude Desktop config, running the `claude` / `gemini` / `q` CLIs (the
142
+ s1ngularity pattern), GitHub token harvesting, and the whole environment
143
+ serialised next to network code
144
+ - Typosquat / slopsquat check against a vendored list of ~7,800 popular npm
145
+ names (`lib/deps/popular-packages.json`, from npm-high-impact), refreshed at
146
+ release time by `npm run refresh:popular`. Nothing is fetched at runtime
147
+ - `--online` (off by default, the only network use): npm registry metadata for
148
+ fresh versions, brand-new packages, publisher changes and deprecations
149
+ - `--pr-comment <file>` and an example workflow
150
+ ([`docs/examples/github-actions-deps.yml`](docs/examples/github-actions-deps.yml))
151
+ that comments the capability diff on PRs that change the lockfile
152
+ - Findings use category `dependency-risk` and source `deps-scan`; SARIF
153
+ results now carry a start line when a finding has one
154
+ - **`penthera install [pkg...]`**: an install firewall on the `deps` engine.
155
+ npm resolves the new tree in a temporary copy with scripts off
156
+ (`--package-lock-only --ignore-scripts`), new tarballs are fetched with
157
+ `npm cache add` and read back only after an integrity check, and the
158
+ capability diff, new install scripts (with their commands) and typosquats
159
+ are shown before anything in the project changes. `penthera.policy.json`
160
+ (`allow` names/globs, `deny` and `requireApproval` capabilities) decides;
161
+ by default new install scripts, AI-config writes, AI CLI invocation, token
162
+ harvesting, obfuscation, typosquats and unreadable packages need approval.
163
+ Interactive by default, `--yes` for CI (installs only when nothing breaches
164
+ the policy), `--dry-run` to report. On approval the reviewed lockfile is
165
+ installed as is, with `--ignore-scripts` unless the scripts were approved
166
+ (on npm 12+, approved scripts are pinned in `package.json` `allowScripts`).
167
+ An abort leaves the project byte for byte unchanged. npm's registry traffic
168
+ is the only egress, and it is printed
169
+ - `scanDeps()` accepts `treeRoot` and a list of cache dirs; the diff output
170
+ carries each changed package's install scripts
171
+ - **Fix recipes** (`lib/fix/`, `penthera fix`): deterministic, local fixes for
172
+ common findings, printed as unified diffs. `penthera fix --plan` plans fixes
173
+ for a fresh repo scan or an earlier `-o` report; `penthera fix --apply <id>`
174
+ applies one after a y/N prompt (or `--yes`), then re-scans to verify. Recipes:
175
+ security headers (Next.js, Express, Fastify, Hono), cookie flags, CORS
176
+ wildcard to an allowlist from `CORS_ALLOWED_ORIGINS`, a rate-limit stub on
177
+ login/auth routes, and a hardcoded secret moved to an env var with
178
+ `.env.example` updated (always with the owner action to rotate it)
179
+ - `applyDiff()` refuses paths outside the repo (symlinks included), `.git`,
180
+ and dirty or non-git trees unless `--allow-dirty`, and writes nothing unless
181
+ every hunk applies. Planning never writes; nothing is committed or pushed
182
+ - Recipes edit only at exact anchors and return no plan when the anchor is
183
+ missing, instead of a risky diff
184
+ - Optional `--fix-model <name>` fallback for findings no recipe matches. It
185
+ talks only to Ollama on `127.0.0.1:11434`, is off by default, and labels its
186
+ output `confidence: "needs-human-review"`, `source: "local-model"`
187
+ - Fixtures for Next.js, Express, Fastify and Hono under `tests/fixtures/fix/`
188
+ - `detectFramework()` is now exported from `lib/whitebox/frameworks.js`
189
+ - **Audience reports** (`--audience developer|owner|auditor`): developer gets
190
+ findings with file:line, fix recipe ids or diffs, and sources; owner gets a
191
+ plain-language summary, what to do first, owner actions and the trend from
192
+ memory; auditor gets scope and method (linking `docs/METHODOLOGY.md`),
193
+ confidence labels with provenance per finding, egress and inconclusive
194
+ probes, suppressions with who and why, and NIS2 and OWASP mapping tables.
195
+ The default report is unchanged
196
+ - **User templates** (`--report-template file.md`): `{{a.b}}`,
197
+ `{{#each}}`, `{{#if}}`/`{{else}}`, nothing else. Values are HTML- and
198
+ Markdown-escaped; no helpers, code, includes or prototype access. Written from
199
+ scratch (`src/report/template.js`) and fuzz-tested against injection
200
+ - **Fix timeline** in the audience reports: found → fixed → verified with time
201
+ to fix, from an agent loop run (`loopRun`) or memory fix events
202
+ (`lib/memory/history.js`, also the owner report's trend)
203
+ - **Evidence bundle** (`--evidence <dir>`, `--sign-key <ssh-key>`): JSON,
204
+ Markdown, SARIF, the three audience reports, a custom template report, the
205
+ readiness report and SBOM when requested, and `manifest.json` (tool version,
206
+ command line with secrets redacted, timestamp, target, git commit, SHA-256 of
207
+ every file). Signing uses `ssh-keygen -Y sign -n penthera-evidence`; no new
208
+ dependency. `penthera evidence verify <dir> [--allowed-signers <file>]`
209
+ recomputes every hash, rejects changed, missing, extra and symlinked files,
210
+ and checks the signature. See `docs/evidence.md`
211
+ - **`penthera agent --loop`** (`src/agent/`): the local scan → triage → plan →
212
+ patch → verify → learn loop. Fixes are applied one at a time on a local
213
+ branch `penthera/fix-<yyyymmdd-hhmm>`, verified by a re-scan (and the
214
+ project's tests with `--run-tests`), committed when verified and reverted
215
+ when not. Every diff needs `y/n/skip/quit` approval; `--auto low` auto-applies
216
+ only recipes memory shows verified 3+ times with no failures on the
217
+ framework. Stops when nothing fixable is left or at `--max-iterations`
218
+ (default 5). `-o` writes a `loopRun` JSON (iterations, fixes, time per step),
219
+ also appended to memory as a new `loop_run` event
220
+ - The loop runs git only through an allow-list of local subcommands with
221
+ `protocol.allow=never`, no lazy fetch and the repo's hooks disabled; it never
222
+ pushes, fetches or opens pull requests. Scans run in one offline egress
223
+ context (the `--url` host only; Ollama on 127.0.0.1 only with
224
+ `--fix-model`). It refuses a dirty tree or detached HEAD unless
225
+ `--allow-dirty`, and Ctrl-C leaves only verified commits on the fix branch
226
+
227
+ ### Docs
228
+
229
+ - Sharper positioning: "the routine work of a security team, on your own
230
+ machine". The README now opens with what Penthera does for you, why you can
231
+ trust it, and a factual table of how it fits next to Socket, Snyk, GitHub,
232
+ OWASP ZAP and Anthropic's OSS Scanner. The package, plugin, skill and
233
+ repository descriptions match.
234
+ - Funding and partnership drafts under `docs/funding/`: an NLnet application
235
+ draft, GitHub Sponsors tiers, a partner brief, and a due diligence checklist.
236
+ - CONTRIBUTING: commits are signed off under the Developer Certificate of Origin.
237
+ - **`--offline`**: the scan contacts only the target. Recon is skipped, the
238
+ Retire.js check uses the vendored database snapshot, and every other host is
239
+ refused by the HTTP client (redirects may stay within the target's
240
+ registrable domain). Groundwork for the local agent loop.
241
+ - **Vendored Retire.js database** (`lib/data/retire-jsrepository.json.gz`,
242
+ 72 KB gzipped) with source URL, date and SHA-256 in a meta file.
243
+ `npm run refresh:retire-db` updates it at release time, and
244
+ `npm run refresh:retire-db -- --check` reports drift from upstream. Online
245
+ scans fall back to it when the live download fails.
246
+ - **Third-party egress report**: every scan lists the hosts it contacted
247
+ besides the target, in the terminal summary and in a "Network egress" section
248
+ of the Markdown report (`result.egress` in JSON).
249
+ - **`--user-agent <ua>`** to override the new default User-Agent.
250
+
251
+ ### Changed
252
+
253
+ - Every request now sends `User-Agent: penthera/<version>
254
+ (+https://github.com/danoszz/penthera)` instead of Node's default (#7;
255
+ first added by @Nurana100 in #20, our first outside contribution).
256
+ - Probe failures (timeouts, connection errors, refused hosts) are recorded as
257
+ `result.errors[]` (`{ probe, url, message, count }`). The terminal summary
258
+ shows "N probes inconclusive", and the Markdown report lists them under
259
+ "Inconclusive probes" (#8).
260
+ - All network calls go through one client in `src/utils/http.js`, which owns
261
+ timeouts, the User-Agent and egress policy. The per-module fetch wrappers in
262
+ the crawler, injection, fuzzer, template, recon, parameter, Retire.js and
263
+ adaptive-probe modules are gone.
264
+ - **Local learning store** (`lib/memory/`), the memory behind the v1.4 local
265
+ loop. `--memory` records each scan under `~/.penthera/memory/<project-id>/`
266
+ (or `PENTHERA_HOME`) as an append-only, hash-chained event log, then uses it
267
+ to suppress findings marked as false positives and to rank findings within a
268
+ severity. Suppressed findings are listed in their own section with who
269
+ suppressed them and why. Memory is never read from the scanned repo and
270
+ nothing in it is ever executed; a broken chain means memory is not applied.
271
+ See [docs/local-loop.md](docs/local-loop.md) for the threat model
272
+ - `penthera memory show | verify | forget [--all] | suppress <id> --reason <text>`
273
+ - Optional repo file `penthera.suppress.json` for shared suppressions. Every
274
+ finding it hides is labelled "suppressed by repo file" with its reason
275
+
7
276
  ## [1.2.0] - 2026-10-19
8
277
 
9
278
  The first step toward securing the AI toolchain itself: catch AI provider keys,