Skip to content

Commit 7480a90

Browse files
docs: Add 'Try in Playground' links to cookbook examples (#776)
<img width="1008" height="316" alt="image" src="https://github.com/user-attachments/assets/9a24be2e-f473-4438-ab0a-9d152e73548e" />
1 parent a7d90a9 commit 7480a90

4 files changed

Lines changed: 183 additions & 28 deletions

File tree

docs/docs/cookbook/cel.mdx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ sidebar_position: 1
55
# Common Expression Language (CEL)
66

77
import AddedBadge from "@site/src/components/AddedBadge/AddedBadge";
8+
import PlaygroundLink from "@site/src/components/CELPlayground/PlaygroundLink";
89

910
This page lists well-known and/or community-contributed CEL expressions.
1011

@@ -23,6 +24,20 @@ before the provided date. This is particularly useful when attached to a
2324
target.signing_time >= timestamp('2025-05-31T00:00:00Z')
2425
```
2526

27+
<PlaygroundLink
28+
expression={`target.signing_time >= timestamp('2025-05-31T00:00:00Z')`}
29+
context={`
30+
target:
31+
signing_time: "2025-06-01T00:00:00Z"
32+
args:
33+
- "--version"
34+
envs:
35+
HOME: "/Users/admin"
36+
euid: 501
37+
cwd: "/Applications"
38+
`}
39+
/>
40+
2641
## Prevent users from disabling gatekeeper
2742

2843
Create a signing ID rule for `platform:com.apple.spctl` and attach the following CEL program
@@ -37,6 +52,26 @@ Create a signing ID rule for `platform:com.apple.spctl` and attach the following
3752
].exists(flag, flag in args) ? BLOCKLIST : ALLOWLIST
3853
```
3954

55+
<PlaygroundLink
56+
expression={`
57+
[
58+
'--global-disable',
59+
'--master-disable',
60+
'--disable',
61+
'--add',
62+
'--remove'
63+
].exists(flag, flag in args) ? BLOCKLIST : ALLOWLIST
64+
`}
65+
context={`
66+
args:
67+
- "--master-disable"
68+
envs:
69+
HOME: "/Users/admin"
70+
euid: 0
71+
cwd: "/Users/admin"
72+
`}
73+
/>
74+
4075
## Prevent Timestomping of LaunchAgents and LaunchDaemons <AddedBadge added={"Santa 2025.8"} />
4176

4277
Malware like those produced by the Chollima groups use "timestomping" to reset the
@@ -54,6 +89,24 @@ args.exists(arg, arg in [
5489
]) && args.join(" ").contains("Library/Launch") ? BLOCKLIST : ALLOWLIST
5590
```
5691

92+
<PlaygroundLink
93+
expression={`
94+
args.exists(arg, arg in [
95+
'-a', '-m', '-r', '-A', '-t'
96+
]) && args.join(" ").contains("Library/Launch") ? BLOCKLIST : ALLOWLIST
97+
`}
98+
context={`
99+
args:
100+
- "-t"
101+
- "202301010000"
102+
- "/Users/admin/Library/LaunchAgents/com.malware.plist"
103+
envs:
104+
HOME: "/Users/admin"
105+
euid: 501
106+
cwd: "/Users/admin"
107+
`}
108+
/>
109+
57110
Note this will not stop using the system calls directly or otherwise
58111
programmatically modifying the timestamps. Also this won't cover modifications
59112
if the process' current working directory is already in the LaunchDaemons /
@@ -77,6 +130,26 @@ Program
77130
".*\\W+display\\W+dialog.*") ? BLOCKLIST : ALLOWLIST
78131
```
79132

133+
<PlaygroundLink
134+
expression={`
135+
(
136+
args.join(" ").lowerAscii().matches(".*\\\\W+with\\\\W+hidden\\\\W+answer.*") ||
137+
args.join(" ").lowerAscii().contains("password")
138+
) &&
139+
args.join(" ").lowerAscii().matches(
140+
".*\\\\W+display\\\\W+dialog.*") ? BLOCKLIST : ALLOWLIST
141+
`}
142+
context={`
143+
args:
144+
- "-e"
145+
- 'display dialog "Enter your password" with hidden answer default answer ""'
146+
envs:
147+
HOME: "/Users/admin"
148+
euid: 501
149+
cwd: "/Users/admin"
150+
`}
151+
/>
152+
80153
Note: This will not stop obfuscated osascript that's evaluated at runtime or
81154
any other malicious behavior triggered through osascript. For better security
82155
block osascript all together if you can. Be aware software like the Google
@@ -99,6 +172,22 @@ args.join(" ").contains("-setremotelogin on") ||
99172
args.join(" ").contains("-setremoteappleevents on") ? BLOCKLIST : ALLOWLIST
100173
```
101174

175+
<PlaygroundLink
176+
expression={`
177+
args.join(" ").contains("-setremotelogin on") ||
178+
args.join(" ").contains("-setremoteappleevents on") ? BLOCKLIST : ALLOWLIST
179+
`}
180+
context={`
181+
args:
182+
- "-setremotelogin"
183+
- "on"
184+
envs:
185+
HOME: "/Users/admin"
186+
euid: 0
187+
cwd: "/Users/admin"
188+
`}
189+
/>
190+
102191
## Prevent Users from Taking and Mounting Time Machine Snapshots
103192

104193
As was presented at [Kawaiicon 2025](https://kawaiicon.org/) by [Calum Hall](https://www.youtube.com/watch?v=hIeNuqq12sk&t=1390s), Time Machine snapshots can be used to bypass [File Access Authorization rules](https://www.youtube.com/watch?v=hIeNuqq12sk&t=1390s).
@@ -110,6 +199,18 @@ You can stop the taking of local snapshots by creating a signing ID for
110199
'localsnapshot' in args ? BLOCKLIST : ALLOWLIST
111200
```
112201

202+
<PlaygroundLink
203+
expression={`'localsnapshot' in args ? BLOCKLIST : ALLOWLIST`}
204+
context={`
205+
args:
206+
- "localsnapshot"
207+
envs:
208+
HOME: "/Users/admin"
209+
euid: 0
210+
cwd: "/Users/admin"
211+
`}
212+
/>
213+
113214
This will break taking local snapshots via the command line. Alternatively if
114215
you need to still be able to take time machine snapshots but don't want users
115216
to mount them locally you can stop the mount of local snapshots with a signing
@@ -120,3 +221,21 @@ ID rule `platform:com.apple.mount_apfs` with the following CEL program
120221
('-s' in args &&
121222
args.exists(arg, arg.contains("com.apple.TimeMachine."))) ? BLOCKLIST : ALLOWLIST
122223
```
224+
225+
<PlaygroundLink
226+
expression={`
227+
('-s' in args &&
228+
args.exists(arg, arg.contains("com.apple.TimeMachine."))) ? BLOCKLIST : ALLOWLIST
229+
`}
230+
context={`
231+
args:
232+
- "-s"
233+
- "com.apple.TimeMachine.2025-06-01-120000.local"
234+
- "/"
235+
- "/tmp/snapshot"
236+
envs:
237+
HOME: "/Users/admin"
238+
euid: 0
239+
cwd: "/Users/admin"
240+
`}
241+
/>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { encodePlaygroundState } from "./encoding";
2+
3+
function dedent(s: string): string {
4+
const lines = s.split("\n");
5+
// Remove leading/trailing empty lines
6+
while (lines.length && lines[0].trim() === "") lines.shift();
7+
while (lines.length && lines[lines.length - 1].trim() === "") lines.pop();
8+
// Find minimum indentation across non-empty lines
9+
const indent = Math.min(
10+
...lines.filter((l) => l.trim()).map((l) => l.match(/^ */)![0].length),
11+
);
12+
return lines.map((l) => l.slice(indent)).join("\n");
13+
}
14+
15+
export default function PlaygroundLink({
16+
expression,
17+
context,
18+
}: {
19+
expression: string;
20+
context: string;
21+
}) {
22+
const hash = encodePlaygroundState(dedent(expression), dedent(context));
23+
return (
24+
<div className="flex justify-end mb-3">
25+
<a
26+
href={`/cookbook/cel-playground#${hash}`}
27+
className="inline-block px-2 py-0.5 rounded border border-border text-xs font-medium no-underline hover:bg-secondary transition-colors"
28+
>
29+
Try in Playground →
30+
</a>
31+
</div>
32+
);
33+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export function encodePlaygroundState(expr: string, yaml: string): string {
2+
const bytes = new TextEncoder().encode(JSON.stringify({ e: expr, c: yaml }));
3+
let binary = "";
4+
for (const b of bytes) {
5+
binary += String.fromCharCode(b);
6+
}
7+
return btoa(binary);
8+
}
9+
10+
export function decodePlaygroundState(
11+
hash: string,
12+
): { expression: string; context: string } | null {
13+
try {
14+
const binary = atob(hash);
15+
const bytes = new Uint8Array(binary.length);
16+
for (let i = 0; i < binary.length; i++) {
17+
bytes[i] = binary.charCodeAt(i);
18+
}
19+
const data = JSON.parse(new TextDecoder().decode(bytes));
20+
if (typeof data.e === "string" && typeof data.c === "string") {
21+
return { expression: data.e, context: data.c };
22+
}
23+
} catch {
24+
// ignore malformed hash
25+
}
26+
return null;
27+
}

docs/src/components/CELPlayground/index.tsx

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import {
1111
DEFAULT_YAML,
1212
type EvalResult,
1313
} from "./eval";
14+
import {
15+
encodePlaygroundState,
16+
decodePlaygroundState,
17+
} from "./encoding";
1418

1519
const commonEditorOptions = {
1620
minimap: { enabled: false },
@@ -36,34 +40,6 @@ const dataEditorOptions = {
3640
tabSize: 2,
3741
};
3842

39-
function encodePlaygroundState(expr: string, yaml: string): string {
40-
const bytes = new TextEncoder().encode(JSON.stringify({ e: expr, c: yaml }));
41-
let binary = "";
42-
for (const b of bytes) {
43-
binary += String.fromCharCode(b);
44-
}
45-
return btoa(binary);
46-
}
47-
48-
function decodePlaygroundState(
49-
hash: string,
50-
): { expression: string; context: string } | null {
51-
try {
52-
const binary = atob(hash);
53-
const bytes = new Uint8Array(binary.length);
54-
for (let i = 0; i < binary.length; i++) {
55-
bytes[i] = binary.charCodeAt(i);
56-
}
57-
const data = JSON.parse(new TextDecoder().decode(bytes));
58-
if (typeof data.e === "string" && typeof data.c === "string") {
59-
return { expression: data.e, context: data.c };
60-
}
61-
} catch {
62-
// ignore malformed hash
63-
}
64-
return null;
65-
}
66-
6743
export default function CELPlayground() {
6844
const { colorMode } = useColorMode();
6945
const [expression, setExpression] = useState(DEFAULT_EXPRESSION);

0 commit comments

Comments
 (0)