@taliesinb/mash 0.0.0-stage → 0.1.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 (203) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -2
  3. package/bin/mash.ts +253 -0
  4. package/dist/bin/mash.d.ts +39 -0
  5. package/dist/bin/mash.d.ts.map +1 -0
  6. package/dist/bin/mash.js +281 -0
  7. package/dist/bin/mash.js.map +1 -0
  8. package/dist/src/analysis/analyze.d.ts +97 -0
  9. package/dist/src/analysis/analyze.d.ts.map +1 -0
  10. package/dist/src/analysis/analyze.js +1406 -0
  11. package/dist/src/analysis/analyze.js.map +1 -0
  12. package/dist/src/analysis/fs.d.ts +39 -0
  13. package/dist/src/analysis/fs.d.ts.map +1 -0
  14. package/dist/src/analysis/fs.js +117 -0
  15. package/dist/src/analysis/fs.js.map +1 -0
  16. package/dist/src/analysis/outputs.d.ts +16 -0
  17. package/dist/src/analysis/outputs.d.ts.map +1 -0
  18. package/dist/src/analysis/outputs.js +277 -0
  19. package/dist/src/analysis/outputs.js.map +1 -0
  20. package/dist/src/analysis/value.d.ts +76 -0
  21. package/dist/src/analysis/value.d.ts.map +1 -0
  22. package/dist/src/analysis/value.js +248 -0
  23. package/dist/src/analysis/value.js.map +1 -0
  24. package/dist/src/behaviors/defs/core.d.ts +15 -0
  25. package/dist/src/behaviors/defs/core.d.ts.map +1 -0
  26. package/dist/src/behaviors/defs/core.js +1084 -0
  27. package/dist/src/behaviors/defs/core.js.map +1 -0
  28. package/dist/src/behaviors/defs/devtools.d.ts +9 -0
  29. package/dist/src/behaviors/defs/devtools.d.ts.map +1 -0
  30. package/dist/src/behaviors/defs/devtools.js +432 -0
  31. package/dist/src/behaviors/defs/devtools.js.map +1 -0
  32. package/dist/src/behaviors/defs/macos.d.ts +12 -0
  33. package/dist/src/behaviors/defs/macos.d.ts.map +1 -0
  34. package/dist/src/behaviors/defs/macos.js +454 -0
  35. package/dist/src/behaviors/defs/macos.js.map +1 -0
  36. package/dist/src/behaviors/defs/tools.d.ts +9 -0
  37. package/dist/src/behaviors/defs/tools.d.ts.map +1 -0
  38. package/dist/src/behaviors/defs/tools.js +639 -0
  39. package/dist/src/behaviors/defs/tools.js.map +1 -0
  40. package/dist/src/behaviors/dsl.d.ts +95 -0
  41. package/dist/src/behaviors/dsl.d.ts.map +1 -0
  42. package/dist/src/behaviors/dsl.js +267 -0
  43. package/dist/src/behaviors/dsl.js.map +1 -0
  44. package/dist/src/behaviors/index.d.ts +12 -0
  45. package/dist/src/behaviors/index.d.ts.map +1 -0
  46. package/dist/src/behaviors/index.js +18 -0
  47. package/dist/src/behaviors/index.js.map +1 -0
  48. package/dist/src/behaviors/registry.d.ts +45 -0
  49. package/dist/src/behaviors/registry.d.ts.map +1 -0
  50. package/dist/src/behaviors/registry.js +266 -0
  51. package/dist/src/behaviors/registry.js.map +1 -0
  52. package/dist/src/behaviors/roles.d.ts +29 -0
  53. package/dist/src/behaviors/roles.d.ts.map +1 -0
  54. package/dist/src/behaviors/roles.js +198 -0
  55. package/dist/src/behaviors/roles.js.map +1 -0
  56. package/dist/src/behaviors/types.d.ts +120 -0
  57. package/dist/src/behaviors/types.d.ts.map +1 -0
  58. package/dist/src/behaviors/types.js +12 -0
  59. package/dist/src/behaviors/types.js.map +1 -0
  60. package/dist/src/index.d.ts +27 -0
  61. package/dist/src/index.d.ts.map +1 -0
  62. package/dist/src/index.js +24 -0
  63. package/dist/src/index.js.map +1 -0
  64. package/dist/src/integrations/deepsea.d.ts +121 -0
  65. package/dist/src/integrations/deepsea.d.ts.map +1 -0
  66. package/dist/src/integrations/deepsea.js +199 -0
  67. package/dist/src/integrations/deepsea.js.map +1 -0
  68. package/dist/src/machine.d.ts +139 -0
  69. package/dist/src/machine.d.ts.map +1 -0
  70. package/dist/src/machine.js +400 -0
  71. package/dist/src/machine.js.map +1 -0
  72. package/dist/src/parser/ast.d.ts +369 -0
  73. package/dist/src/parser/ast.d.ts.map +1 -0
  74. package/dist/src/parser/ast.js +17 -0
  75. package/dist/src/parser/ast.js.map +1 -0
  76. package/dist/src/parser/parser.d.ts +48 -0
  77. package/dist/src/parser/parser.d.ts.map +1 -0
  78. package/dist/src/parser/parser.js +1950 -0
  79. package/dist/src/parser/parser.js.map +1 -0
  80. package/dist/src/policy/effects.d.ts +86 -0
  81. package/dist/src/policy/effects.d.ts.map +1 -0
  82. package/dist/src/policy/effects.js +53 -0
  83. package/dist/src/policy/effects.js.map +1 -0
  84. package/dist/src/policy/policy.d.ts +130 -0
  85. package/dist/src/policy/policy.d.ts.map +1 -0
  86. package/dist/src/policy/policy.js +532 -0
  87. package/dist/src/policy/policy.js.map +1 -0
  88. package/dist/src/printer/doc.d.ts +151 -0
  89. package/dist/src/printer/doc.d.ts.map +1 -0
  90. package/dist/src/printer/doc.js +409 -0
  91. package/dist/src/printer/doc.js.map +1 -0
  92. package/dist/src/printer/highlight.d.ts +53 -0
  93. package/dist/src/printer/highlight.d.ts.map +1 -0
  94. package/dist/src/printer/highlight.js +339 -0
  95. package/dist/src/printer/highlight.js.map +1 -0
  96. package/dist/src/printer/html.d.ts +72 -0
  97. package/dist/src/printer/html.d.ts.map +1 -0
  98. package/dist/src/printer/html.js +423 -0
  99. package/dist/src/printer/html.js.map +1 -0
  100. package/dist/src/printer/print.d.ts +63 -0
  101. package/dist/src/printer/print.d.ts.map +1 -0
  102. package/dist/src/printer/print.js +484 -0
  103. package/dist/src/printer/print.js.map +1 -0
  104. package/dist/src/printer/summary.d.ts +50 -0
  105. package/dist/src/printer/summary.d.ts.map +1 -0
  106. package/dist/src/printer/summary.js +621 -0
  107. package/dist/src/printer/summary.js.map +1 -0
  108. package/dist/src/trace/events.d.ts +183 -0
  109. package/dist/src/trace/events.d.ts.map +1 -0
  110. package/dist/src/trace/events.js +9 -0
  111. package/dist/src/trace/events.js.map +1 -0
  112. package/dist/src/trace/progress.d.ts +58 -0
  113. package/dist/src/trace/progress.d.ts.map +1 -0
  114. package/dist/src/trace/progress.js +83 -0
  115. package/dist/src/trace/progress.js.map +1 -0
  116. package/dist/src/vm/arith.d.ts +29 -0
  117. package/dist/src/vm/arith.d.ts.map +1 -0
  118. package/dist/src/vm/arith.js +258 -0
  119. package/dist/src/vm/arith.js.map +1 -0
  120. package/dist/src/vm/builtins.d.ts +31 -0
  121. package/dist/src/vm/builtins.d.ts.map +1 -0
  122. package/dist/src/vm/builtins.js +1773 -0
  123. package/dist/src/vm/builtins.js.map +1 -0
  124. package/dist/src/vm/emulate.d.ts +75 -0
  125. package/dist/src/vm/emulate.d.ts.map +1 -0
  126. package/dist/src/vm/emulate.js +380 -0
  127. package/dist/src/vm/emulate.js.map +1 -0
  128. package/dist/src/vm/expand.d.ts +68 -0
  129. package/dist/src/vm/expand.d.ts.map +1 -0
  130. package/dist/src/vm/expand.js +744 -0
  131. package/dist/src/vm/expand.js.map +1 -0
  132. package/dist/src/vm/external.d.ts +34 -0
  133. package/dist/src/vm/external.d.ts.map +1 -0
  134. package/dist/src/vm/external.js +228 -0
  135. package/dist/src/vm/external.js.map +1 -0
  136. package/dist/src/vm/fifo.d.ts +66 -0
  137. package/dist/src/vm/fifo.d.ts.map +1 -0
  138. package/dist/src/vm/fifo.js +232 -0
  139. package/dist/src/vm/fifo.js.map +1 -0
  140. package/dist/src/vm/interp.d.ts +209 -0
  141. package/dist/src/vm/interp.d.ts.map +1 -0
  142. package/dist/src/vm/interp.js +1641 -0
  143. package/dist/src/vm/interp.js.map +1 -0
  144. package/dist/src/vm/io.d.ts +180 -0
  145. package/dist/src/vm/io.d.ts.map +1 -0
  146. package/dist/src/vm/io.js +502 -0
  147. package/dist/src/vm/io.js.map +1 -0
  148. package/dist/src/vm/pattern.d.ts +46 -0
  149. package/dist/src/vm/pattern.d.ts.map +1 -0
  150. package/dist/src/vm/pattern.js +498 -0
  151. package/dist/src/vm/pattern.js.map +1 -0
  152. package/dist/src/vm/proc.d.ts +94 -0
  153. package/dist/src/vm/proc.d.ts.map +1 -0
  154. package/dist/src/vm/proc.js +130 -0
  155. package/dist/src/vm/proc.js.map +1 -0
  156. package/dist/src/vm/state.d.ts +104 -0
  157. package/dist/src/vm/state.d.ts.map +1 -0
  158. package/dist/src/vm/state.js +233 -0
  159. package/dist/src/vm/state.js.map +1 -0
  160. package/dist/src/vm/test.d.ts +11 -0
  161. package/dist/src/vm/test.d.ts.map +1 -0
  162. package/dist/src/vm/test.js +277 -0
  163. package/dist/src/vm/test.js.map +1 -0
  164. package/package.json +63 -4
  165. package/src/analysis/analyze.ts +1395 -0
  166. package/src/analysis/fs.ts +121 -0
  167. package/src/analysis/outputs.ts +242 -0
  168. package/src/analysis/value.ts +264 -0
  169. package/src/behaviors/defs/core.ts +970 -0
  170. package/src/behaviors/defs/devtools.ts +400 -0
  171. package/src/behaviors/defs/macos.ts +426 -0
  172. package/src/behaviors/defs/tools.ts +596 -0
  173. package/src/behaviors/dsl.ts +293 -0
  174. package/src/behaviors/index.ts +19 -0
  175. package/src/behaviors/registry.ts +261 -0
  176. package/src/behaviors/roles.ts +187 -0
  177. package/src/behaviors/types.ts +127 -0
  178. package/src/index.ts +27 -0
  179. package/src/integrations/deepsea.ts +259 -0
  180. package/src/machine.ts +483 -0
  181. package/src/parser/ast.ts +472 -0
  182. package/src/parser/parser.ts +1826 -0
  183. package/src/policy/effects.ts +102 -0
  184. package/src/policy/policy.ts +578 -0
  185. package/src/printer/doc.ts +454 -0
  186. package/src/printer/highlight.ts +283 -0
  187. package/src/printer/html.ts +441 -0
  188. package/src/printer/print.ts +528 -0
  189. package/src/printer/summary.ts +601 -0
  190. package/src/trace/events.ts +102 -0
  191. package/src/trace/progress.ts +119 -0
  192. package/src/vm/arith.ts +236 -0
  193. package/src/vm/builtins.ts +1590 -0
  194. package/src/vm/emulate.ts +337 -0
  195. package/src/vm/expand.ts +721 -0
  196. package/src/vm/external.ts +265 -0
  197. package/src/vm/fifo.ts +226 -0
  198. package/src/vm/interp.ts +1623 -0
  199. package/src/vm/io.ts +570 -0
  200. package/src/vm/pattern.ts +477 -0
  201. package/src/vm/proc.ts +147 -0
  202. package/src/vm/state.ts +267 -0
  203. package/src/vm/test.ts +253 -0
@@ -0,0 +1,426 @@
1
+ /**
2
+ * Behavior definitions for macOS system tools (most also describe their BSD
3
+ * or Linux namesakes well enough): preferences and property lists, launchd,
4
+ * code signing, network configuration, Spotlight, disk images, diagnostics.
5
+ *
6
+ * Query forms are modeled precisely; forms that change system state the
7
+ * policy has no vocabulary for (network settings, services, firewall) are
8
+ * `unknown` with `complete: false`.
9
+ */
10
+
11
+ import type { AValue } from '../../analysis/value.ts'
12
+ import { concat, concrete, hole, known, knownPrefix } from '../../analysis/value.ts'
13
+ import type { Effect } from '../../policy/effects.ts'
14
+ import { define, type Def } from '../dsl.ts'
15
+ import type { Behavior, BehaviorHelpers, Invocation } from '../types.ts'
16
+
17
+ const args = (inv: Invocation): AValue[] => inv.argv.slice(1)
18
+ const isDash = (v: AValue): boolean => concrete(v) === '-'
19
+
20
+ const QUERY: Def = { anyFlags: true, operands: 'arg*', stdin: false }
21
+ const opaque = (detail: string): Def => ({ anyFlags: true, operands: 'arg*', opaque: detail, stdin: 'maybe' })
22
+
23
+ /** Commands that only print system information. */
24
+ function info(names: string[], summary: string): Behavior {
25
+ return { names, summary, analyze: () => ({ effects: [], readsStdin: false }) }
26
+ }
27
+
28
+ /** A tool whose first word decides: query words are read-only, others change system state. */
29
+ function queryOrChange(names: string[], summary: string, query: RegExp, change: string): Behavior {
30
+ return {
31
+ names,
32
+ summary,
33
+ analyze(inv, h) {
34
+ const a = args(inv)
35
+ const first = a.find((x) => knownPrefix(x) !== '' && !/^-(n|v|q)$/.test(concrete(x) ?? ''))
36
+ const w = first === undefined ? '' : concrete(first)
37
+ if (w === undefined) return { effects: [{ kind: 'unknown', detail: `${inv.name} with unknown arguments` }], readsStdin: false, complete: false }
38
+ if (first && /^[a-z]/.test(w)) h.role(first, 'subcommand')
39
+ if (query.test(w)) return { effects: [], readsStdin: false }
40
+ return { effects: [{ kind: 'unknown', detail: `${inv.name} ${w}: ${change}` }], readsStdin: false, complete: false }
41
+ },
42
+ }
43
+ }
44
+
45
+ const home = (h: BehaviorHelpers, rest: string): AValue => concat(h.home() ?? known('~'), known(rest))
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // preferences and property lists
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /** defaults [-currentHost | -host H] VERB [DOMAIN] …; a domain may be a plist path. */
52
+ const defaults: Behavior = {
53
+ names: ['defaults'],
54
+ summary: 'user preferences',
55
+ analyze(inv, h) {
56
+ const a = [...args(inv)]
57
+ while (a.length > 0 && /^-(currentHost|host)$/.test(concrete(a[0]) ?? '')) a.splice(0, concrete(a[0]) === '-host' ? 2 : 1)
58
+ const verb = concrete(a[0] ?? known('')) ?? '?'
59
+ if (a[0]) h.role(a[0], 'subcommand')
60
+ const rest = a.slice(1).filter((x) => !/^-(g|globalDomain|app)$/.test(concrete(x) ?? ''))
61
+ const domain = rest[0]
62
+ const prefs = home(h, '/Library/Preferences')
63
+ // a domain given as a path names that plist (without .plist)
64
+ const target = (): AValue => {
65
+ if (!domain || !/^[/~.]/.test(knownPrefix(domain))) return prefs
66
+ const s = concrete(domain)
67
+ return s !== undefined && !s.endsWith('.plist') ? h.resolve(known(s + '.plist')) : h.resolve(domain)
68
+ }
69
+ const isPrefs = (v: AValue): boolean => v === prefs
70
+ if (/^(read|read-type|find|domains|help)$/.test(verb)) {
71
+ const t = target()
72
+ return { effects: [{ kind: 'read', path: t, ...(isPrefs(t) ? { recursive: true } : {}) }], readsStdin: false }
73
+ }
74
+ if (verb === 'export') {
75
+ const t = target()
76
+ const out = rest[1]
77
+ const effects: Effect[] = [{ kind: 'read', path: t, ...(isPrefs(t) ? { recursive: true } : {}) }]
78
+ if (out && !isDash(out)) effects.push({ kind: 'write', path: h.resolve(out) })
79
+ return { effects, readsStdin: false }
80
+ }
81
+ if (/^(write|delete|rename|import)$/.test(verb)) {
82
+ const t = target()
83
+ const effects: Effect[] = [{ kind: 'write', path: t, ...(isPrefs(t) ? { recursive: true } : {}) }]
84
+ if (verb === 'import' && rest[1]) {
85
+ if (isDash(rest[1])) return { effects, readsStdin: true }
86
+ effects.push({ kind: 'read', path: h.resolve(rest[1]) })
87
+ }
88
+ return { effects, readsStdin: false }
89
+ }
90
+ return { effects: [{ kind: 'unknown', detail: `defaults ${verb}` }], readsStdin: false, complete: false }
91
+ },
92
+ }
93
+
94
+ /** plutil: -p/-lint/-type read; -convert/-extract/-insert/-replace/-remove rewrite in place unless -o. */
95
+ const PLUTIL_MODES: Record<string, number> = { '-p': 0, '-lint': 0, '-help': 0, '-type': 1, '-convert': 1, '-extract': 2, '-insert': 3, '-replace': 3, '-remove': 1, '-create': 1 }
96
+ const plutil: Behavior = {
97
+ names: ['plutil'],
98
+ summary: 'property list utility',
99
+ analyze(inv, h) {
100
+ const a = args(inv)
101
+ let mode = '-lint'
102
+ let out: AValue | undefined
103
+ let ext: AValue | undefined
104
+ const files: AValue[] = []
105
+ let complete = true
106
+ for (let i = 0; i < a.length; i++) {
107
+ const s = concrete(a[i])
108
+ if (s === undefined) {
109
+ files.push(a[i])
110
+ continue
111
+ }
112
+ if (s in PLUTIL_MODES) {
113
+ mode = s
114
+ // `-insert KEY -TYPE VALUE`, `-extract KEYPATH FMT`, `-convert FMT`
115
+ i += PLUTIL_MODES[s]
116
+ continue
117
+ }
118
+ if (s === '-o') out = a[++i]
119
+ else if (s === '-e') ext = a[++i]
120
+ else if (s === '-s' || s === '-r' || s === '-n' || s === '--' || s === '-x' || s === '-json' || s === '-xml1' || s === '-binary1' || s === '-raw' || s === '-expect') {
121
+ if (s === '-expect') i++
122
+ } else if (s.startsWith('-') && s !== '-') complete = false
123
+ else files.push(a[i])
124
+ }
125
+ const effects: Effect[] = []
126
+ let readsStdin = false
127
+ for (const f of files) {
128
+ if (isDash(f)) readsStdin = true
129
+ else effects.push({ kind: 'read', path: h.resolve(f) })
130
+ }
131
+ if (!['-p', '-lint', '-help', '-type'].includes(mode)) {
132
+ if (out) {
133
+ if (!isDash(out)) effects.push({ kind: 'write', path: h.resolve(out) })
134
+ } else if (ext) {
135
+ for (const f of files) if (!isDash(f)) effects.push({ kind: 'write', path: concat(h.resolve(f), known('.'), ext) })
136
+ } else for (const f of files) if (!isDash(f)) effects.push({ kind: 'write', path: h.resolve(f) })
137
+ }
138
+ return { effects, readsStdin, complete }
139
+ },
140
+ }
141
+
142
+ /** PlistBuddy [-x] [-c CMD]... FILE */
143
+ const plistBuddy: Behavior = {
144
+ names: ['PlistBuddy'],
145
+ summary: 'edit property lists',
146
+ analyze(inv, h) {
147
+ const a = args(inv)
148
+ const cmds: AValue[] = []
149
+ let file: AValue | undefined
150
+ for (let i = 0; i < a.length; i++) {
151
+ const s = concrete(a[i])
152
+ if (s === '-c') {
153
+ if (a[i + 1]) h.role(a[i + 1], 'code')
154
+ cmds.push(a[++i])
155
+ } else if (s === '-x' || s === '-h') continue
156
+ else file = a[i]
157
+ }
158
+ if (!file) return { effects: [], readsStdin: false }
159
+ const path = h.resolve(file)
160
+ if (cmds.length === 0) return { effects: [{ kind: 'read', path }, { kind: 'write', path, maybe: true }, { kind: 'unknown', detail: 'interactive PlistBuddy commands' }], readsStdin: true, complete: false }
161
+ const effects: Effect[] = [{ kind: 'read', path }]
162
+ let complete = true
163
+ let writes = false
164
+ for (const c of cmds) {
165
+ const t = c ? concrete(c) : undefined
166
+ if (t === undefined) {
167
+ complete = false
168
+ writes = true
169
+ continue
170
+ }
171
+ const verb = t.trim().split(/\s+/)[0]?.toLowerCase() ?? ''
172
+ if (verb === 'print' || verb === 'help' || verb === 'exit') continue
173
+ if (verb === 'merge' || verb === 'import') complete = false // reads another file
174
+ writes = true
175
+ }
176
+ if (writes) effects.push({ kind: 'write', path, ...(complete ? {} : { maybe: true }) })
177
+ return { effects, readsStdin: false, complete }
178
+ },
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // launchd, Xcode, signing
183
+ // ---------------------------------------------------------------------------
184
+
185
+ const launchctl = queryOrChange(['launchctl'], 'launchd services',
186
+ /^(print|print-cache|print-disabled|list|blame|procinfo|hostinfo|examine|dumpstate|dumpjpcategory|managerpid|manageruid|managername|getenv|version|help|error|resolveport|plist)$/, 'starts, stops or configures services')
187
+
188
+ const xcodeSelect = define({
189
+ names: ['xcode-select'], summary: 'active developer directory',
190
+ spec: { short: 'hpvr', shortWithArg: 's', long: { help: false, 'print-path': false, version: false, switch: '=s', reset: '=r', install: false } },
191
+ stdin: false, stdout: { type: 'path' },
192
+ when: { 's|r|install': { opaque: 'xcode-select changes the developer directory or installs tools' } },
193
+ })
194
+
195
+ /** xcrun [--sdk X] [--toolchain X] TOOL ARGS… | --find TOOL | --show-sdk-*. */
196
+ const xcrun: Behavior = {
197
+ names: ['xcrun'],
198
+ summary: 'run or locate developer tools',
199
+ analyze(inv, h) {
200
+ const a = args(inv)
201
+ let i = 0
202
+ for (; i < a.length; i++) {
203
+ const s = concrete(a[i])
204
+ if (s === undefined) return { effects: [{ kind: 'unknown', detail: 'xcrun with unknown arguments' }], readsStdin: 'maybe', complete: false }
205
+ if (s === '--sdk' || s === '-sdk' || s === '--toolchain' || s === '-toolchain') {
206
+ i++
207
+ continue
208
+ }
209
+ if (/^-(-)?(find|f)$/.test(s) || /^--show-sdk-|^--show-toolchain|^-(-)?(version|help|h|kill-cache|k)$/.test(s)) return { effects: [], readsStdin: false, stdout: { type: 'path' } }
210
+ if (/^-(-)?(log|verbose|no-cache|l|v|n|r|run)$/.test(s)) continue
211
+ break
212
+ }
213
+ if (i >= a.length) return { effects: [], readsStdin: false }
214
+ return h.subcommand(a.slice(i))
215
+ },
216
+ }
217
+
218
+ /** codesign: -s/--sign rewrites the operands; -v/-d/--verify/--display read them. */
219
+ const codesign = define({
220
+ names: ['codesign'], summary: 'code signing',
221
+ spec: { short: 'dvfhRoV', shortWithArg: 'siPr', long: { sign: '=s', verify: false, display: false, deep: false, strict: true, force: false, verbose: true, identifier: '=i', entitlements: true, options: true, timestamp: true, 'preserve-metadata': true, keychain: true, requirements: '=r', remove: false, 'remove-signature': false, xml: false, der: false, 'check-notarization': false, 'generate-entitlement-der': false, 'all-architectures': false, prefix: true, 'test-requirement': true, detached: true, bundle: true, file: false, extract: true } },
222
+ operands: 'read*', stdin: false, flags: { entitlements: 'write' },
223
+ when: { 's|remove-signature|remove': { operands: 'write*', flags: { entitlements: 'read', keychain: 'read' } } },
224
+ })
225
+
226
+ const spctl = queryOrChange(['spctl'], 'Gatekeeper assessments', /^(-a|--assess|--status|-s|--test|-t|--list|--help|-h)$/, 'changes the Gatekeeper policy')
227
+
228
+ const security: Behavior = {
229
+ names: ['security'],
230
+ summary: 'keychains and certificates',
231
+ analyze(inv, h) {
232
+ const a = args(inv)
233
+ const sub = concrete(a.find((x) => !knownPrefix(x).startsWith('-')) ?? known('')) ?? '?'
234
+ const keychains = home(h, '/Library/Keychains')
235
+ if (/^(find-|dump-|list-|show-|verify-cert$|error$|help$)/.test(sub) || (sub === 'cms' && a.some((x) => concrete(x) === '-D'))) {
236
+ const effects: Effect[] = [{ kind: 'read', path: keychains, recursive: true }, { kind: 'read', path: known('/Library/Keychains'), recursive: true, maybe: true }]
237
+ if (sub === 'cms') for (let i = 0; i < a.length; i++) if (concrete(a[i]) === '-i' && a[i + 1]) effects.push({ kind: 'read', path: h.resolve(a[i + 1]) })
238
+ return { effects, readsStdin: sub === 'cms' && !a.some((x) => concrete(x) === '-i') }
239
+ }
240
+ return { effects: [{ kind: 'write', path: keychains, recursive: true, maybe: true }, { kind: 'unknown', detail: `security ${sub}` }], readsStdin: 'maybe', complete: false }
241
+ },
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // network
246
+ // ---------------------------------------------------------------------------
247
+
248
+ const networkTools: Behavior[] = [
249
+ queryOrChange(['ipconfig'], 'network interfaces (macOS)', /^(getifaddr|getoption|getpacket|getsummary|getv6packet|ifcount|getiflist|getra|waitall|help)$/, 'changes interface configuration'),
250
+ {
251
+ names: ['ifconfig'],
252
+ summary: 'network interfaces',
253
+ analyze(inv) {
254
+ // `ifconfig`, `ifconfig -a`, `ifconfig -l`, `ifconfig en0`: queries; more words configure the interface
255
+ const ops = args(inv).filter((x) => !/^-[aludLmvrC]+$/.test(concrete(x) ?? ''))
256
+ if (ops.length <= 1) return { effects: [], readsStdin: false }
257
+ return { effects: [{ kind: 'unknown', detail: 'ifconfig configures an interface' }], readsStdin: false, complete: false }
258
+ },
259
+ },
260
+ {
261
+ names: ['arp', 'ndp'],
262
+ summary: 'address resolution tables',
263
+ analyze(inv) {
264
+ const a = args(inv).map((x) => concrete(x))
265
+ if (a.some((x) => x === undefined || /^-[a-zA-Z]*[dsSf]/.test(x))) return { effects: [{ kind: 'unknown', detail: `${inv.name} changes the table` }], readsStdin: false, complete: false }
266
+ return { effects: [], readsStdin: false }
267
+ },
268
+ },
269
+ queryOrChange(['route'], 'routing tables', /^(get|monitor|show|-n)$/, 'changes routes'),
270
+ queryOrChange(['scutil'], 'system configuration', /^(--dns|--proxy|--nwi|--get|-r|-w)$/, 'changes the system configuration'),
271
+ queryOrChange(['networksetup'], 'network settings', /^-(get|list|print|version|help|isBluetooth|show)/, 'changes network settings'),
272
+ queryOrChange(['socketfilterfw'], 'application firewall', /^--(get|list)/, 'changes the firewall'),
273
+ {
274
+ names: ['dscacheutil'],
275
+ summary: 'directory service cache',
276
+ analyze: (inv) => ({ effects: args(inv).some((x) => concrete(x) === '-flushcache') ? [{ kind: 'process', detail: 'flush the DNS cache' }] : [], readsStdin: false }),
277
+ },
278
+ define({ names: ['dns-sd', 'mDNS'], summary: 'Bonjour browsing and registration', anyFlags: true, operands: 'arg*', stdin: false, effects: [{ kind: 'network', detail: 'multicast DNS' }] }),
279
+ define({ names: ['mtr', 'whois'], summary: 'network diagnostics', anyFlags: true, operands: 'url arg*', stdin: false }),
280
+ ]
281
+
282
+ // ---------------------------------------------------------------------------
283
+ // system state and diagnostics
284
+ // ---------------------------------------------------------------------------
285
+
286
+ const systemTools: Behavior[] = [
287
+ info(['system_profiler', 'ioreg', 'lsappinfo', 'footprint', 'vmmap', 'heap', 'leaks', 'fs_usage', 'powermetrics', 'nettop', 'csrutil', 'kextstat', 'machine'], 'print system or process information'),
288
+ define({
289
+ names: ['sysctl'], summary: 'kernel state',
290
+ spec: { short: 'abdehiNnoqxAX', shortWithArg: 'f' }, operands: 'arg*', stdin: false,
291
+ // `name=value` and -w set kernel parameters
292
+ extra: (r, { operands, p }) => (p.flags.has('w') || operands.some((o) => knownPrefix(o).includes('=') || concrete(o) === undefined) ? { ...r, effects: [...r.effects, { kind: 'unknown', detail: 'sysctl sets kernel parameters' }], complete: false } : r),
293
+ }),
294
+ define({
295
+ names: ['memory_pressure'], summary: 'memory pressure', spec: { short: 'Q', shortWithArg: 'lSps' }, stdin: false,
296
+ when: { S: { opaque: 'memory_pressure -S simulates memory pressure' } },
297
+ }),
298
+ queryOrChange(['pmset'], 'power management', /^-g$/, 'changes power settings'),
299
+ queryOrChange(['tmutil'], 'Time Machine', /^(listbackups|latestbackup|status|destinationinfo|isexcluded|machinedirectory|listlocalsnapshots|listlocalsnapshotdates|compare|calculatedrift|uniquesize|version|help|verifychecksums)$/, 'changes backups or their settings'),
300
+ queryOrChange(['diskutil'], 'disks and volumes', /^(list|info|information|activity|listFilesystems|apfs|cs|appleRAID|verifyVolume|verifyDisk|image)$/, 'changes disks or volumes'),
301
+ queryOrChange(['hdiutil'], 'disk images', /^(info|imageinfo|verify|checksum|isencrypted|plugins|help|pmap)$/, 'mounts or creates disk images'),
302
+ queryOrChange(['log'], 'unified logging', /^(show|stream|stats|help)$/, 'changes logging configuration or collects archives'),
303
+ queryOrChange(['lsregister'], 'Launch Services database', /^-(dump|h|help)$/, 'changes the Launch Services database'),
304
+ {
305
+ names: ['sample', 'spindump'],
306
+ summary: 'sample a process',
307
+ analyze(inv, h) {
308
+ // writes the report to -file PATH, else into /tmp
309
+ const a = args(inv)
310
+ const i = a.findIndex((x) => concrete(x) === '-file' || concrete(x) === '-o')
311
+ const out: AValue = i >= 0 && a[i + 1] ? h.resolve(a[i + 1]) : concat(known('/tmp/'), hole('filename', 'sample report'))
312
+ return { effects: [{ kind: 'write', path: out }], readsStdin: false }
313
+ },
314
+ },
315
+ ]
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // files, Spotlight, images, screen
319
+ // ---------------------------------------------------------------------------
320
+
321
+ const mdfind: Behavior = {
322
+ names: ['mdfind'],
323
+ summary: 'Spotlight search',
324
+ analyze(inv, h) {
325
+ const a = args(inv)
326
+ const effects: Effect[] = []
327
+ let complete = true
328
+ for (let i = 0; i < a.length; i++) {
329
+ const s = concrete(a[i])
330
+ if (s === '-onlyin' && a[i + 1]) {
331
+ h.role(a[i + 1], 'dir')
332
+ effects.push({ kind: 'read', path: h.resolve(a[++i]), recursive: true })
333
+ } else if (s === '-name' || s === '-attr' || s === '-s') {
334
+ if (a[i + 1]) h.role(a[i + 1], 'pattern')
335
+ i++
336
+ } else if (s === '-live' || s === '-count' || s === '-0' || s === '-literal' || s === '-interpret' || s === '-reprint') continue
337
+ else if (s !== undefined && s.startsWith('-')) complete = false
338
+ else h.role(a[i], 'pattern')
339
+ }
340
+ // names (not contents) of matching files anywhere in the index
341
+ if (!effects.length) effects.push({ kind: 'read', path: known('/'), recursive: true, maybe: true })
342
+ return { effects, readsStdin: false, complete, stdout: { type: 'path', lines: true } }
343
+ },
344
+ }
345
+
346
+ const mdls = define({
347
+ names: ['mdls'], summary: 'Spotlight metadata',
348
+ spec: { longSingle: true, long: { name: true, raw: false, nullMarker: true, plist: true, r: false } },
349
+ operands: 'read*', flags: { plist: 'write' }, stdin: false,
350
+ })
351
+
352
+ /** sips: queries (-g) read; anything else rewrites the images, or writes --out. */
353
+ const SIPS_QUERY = new Set(['g', 'getProperty', '1', 'oneLine', 'h', 'help', 'H', 'helpProperties', 'v', 'version', 'debug'])
354
+ const sips = define({
355
+ names: ['sips'], summary: 'image processing (macOS)',
356
+ spec: {
357
+ short: '1hHv', shortWithArg: 'gsdrfcpzZioeEmMxj', arity: { s: 2, c: 2, p: 2, z: 2, M: 2 },
358
+ long: { getProperty: '=g', setProperty: '=s', deleteProperty: '=d', rotate: '=r', flip: '=f', cropToHeightWidth: '=c', padToHeightWidth: '=p', resampleHeightWidth: '=z', resampleHeightWidthMax: '=Z', resampleWidth: true, resampleHeight: true, cropOffset: 2, padColor: true, addIcon: '=i', out: '=o', embedProfile: '=e', embedProfileIfNone: '=E', matchTo: '=m', matchToWithIntent: '=M', extractProfile: '=x', extractTag: 2, js: '=j', oneLine: '=1', help: '=h', helpProperties: '=H', version: '=v', debug: false, optimizeColorForSharing: false, verifyColor: false, deleteColorManagementProperties: false, setColorManagementPropertiesFromFile: true },
359
+ },
360
+ operands: 'read*', stdin: false, flags: { e: 'read', E: 'read', m: 'read', x: 'write', extractTag: 'write' },
361
+ extra: (r, { h, p, operands }) => {
362
+ if (p.flags.has('j')) return { ...r, effects: [...r.effects, { kind: 'unknown', detail: 'sips runs a JavaScript file' }], complete: false }
363
+ const modifies = [...p.flags.keys()].some((k) => !SIPS_QUERY.has(k) && !['e', 'E', 'm', 'M', 'x', 'extractTag', 'o'].includes(k)) || ['e', 'E', 'm', 'M'].some((k) => p.flags.has(k))
364
+ if (!modifies) return r
365
+ const out = p.flags.get('o')
366
+ const writes: Effect[] = out && out !== true
367
+ ? [{ kind: 'write', path: h.resolve(out), ...(operands.length > 1 ? { recursive: true } : {}) }]
368
+ : operands.map((o): Effect => ({ kind: 'write', path: h.resolve(o) }))
369
+ if (out && out !== true && operands.length > 1) h.role(out, 'dir')
370
+ return { ...r, effects: [...r.effects, ...writes] }
371
+ },
372
+ })
373
+
374
+ const screencapture = define({
375
+ names: ['screencapture'], summary: 'screenshots (macOS)',
376
+ spec: { short: 'cCdimMoPswWxSaUuvkgbBIrpGJ', shortWithArg: 'RtTlDVBGJf' },
377
+ operands: 'write*', stdin: false,
378
+ when: { c: { operands: 'arg*' }, 'M|P': { effects: [{ kind: 'process', detail: 'opens the capture in an app' }] } },
379
+ })
380
+
381
+ const qlmanage = define({
382
+ names: ['qlmanage'], summary: 'Quick Look', spec: { short: 'pthmrxi', shortWithArg: 'osgc', long: {} },
383
+ operands: 'read*', stdin: false, flags: { o: 'write:dir' },
384
+ when: { p: { opaque: 'qlmanage -p opens a preview window' }, r: { effects: [{ kind: 'process', detail: 'resets Quick Look' }] } },
385
+ })
386
+
387
+ /** Chrome / Chromium: --headless with an output flag renders a page; anything else is the GUI browser. */
388
+ const chrome: Behavior = {
389
+ names: ['Google Chrome', 'google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'chrome', 'Chromium', 'Google Chrome Canary', 'Microsoft Edge', 'Brave Browser'],
390
+ summary: 'web browser',
391
+ analyze(inv, h) {
392
+ const a = args(inv)
393
+ const opt = (name: string): AValue | true | undefined => {
394
+ for (const x of a) {
395
+ const s = concrete(x)
396
+ if (s === `--${name}`) return true
397
+ if (s?.startsWith(`--${name}=`)) return known(s.slice(name.length + 3))
398
+ }
399
+ return undefined
400
+ }
401
+ if (opt('version') || opt('help')) return { effects: [], readsStdin: false }
402
+ const headless = opt('headless') !== undefined
403
+ const effects: Effect[] = [{ kind: 'network', detail: inv.name, maybe: true }]
404
+ const shot = opt('screenshot')
405
+ const pdf = opt('print-to-pdf')
406
+ const profile = opt('user-data-dir')
407
+ effects.push(profile && profile !== true
408
+ ? { kind: 'write', path: h.resolve(profile), recursive: true }
409
+ : { kind: 'write', path: home(h, process.platform === 'darwin' ? '/Library/Application Support' : '/.config'), recursive: true, maybe: true })
410
+ if (shot) effects.push({ kind: 'write', path: shot === true ? concat(inv.cwd, known('/screenshot.png')) : h.resolve(shot) })
411
+ if (pdf) effects.push({ kind: 'write', path: pdf === true ? concat(inv.cwd, known('/output.pdf')) : h.resolve(pdf) })
412
+ for (const x of a) {
413
+ const s = concrete(x)
414
+ if (s !== undefined && !s.startsWith('-') && !/^[a-z]+:/.test(s)) effects.push({ kind: 'read', path: h.resolve(x) })
415
+ if (s?.startsWith('file://')) effects.push({ kind: 'read', path: known(decodeURIComponent(s.slice(7))) })
416
+ }
417
+ const outputOnly = headless && (shot || pdf || opt('dump-dom'))
418
+ if (!outputOnly) effects.push({ kind: 'unknown', detail: `${inv.name} runs a browser` })
419
+ return { effects, readsStdin: false, complete: !!outputOnly && !opt('remote-debugging-port') && !opt('remote-debugging-pipe') }
420
+ },
421
+ }
422
+
423
+ export const macosBehaviors: Behavior[] = [
424
+ defaults, plutil, plistBuddy, launchctl, xcodeSelect, xcrun, codesign, spctl, security,
425
+ ...networkTools, ...systemTools, mdfind, mdls, sips, screencapture, qlmanage, chrome,
426
+ ]