@nebularstreams/overlay-sdk 1.0.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/package.json +44 -0
- package/preview/assets/images/favicon.ico +0 -0
- package/preview/assets/images/favicon.png +0 -0
- package/preview/assets/images/login-right.webp +0 -0
- package/preview/assets/images/nebular.color.100h.png +0 -0
- package/preview/assets/images/nebular.color.white.100h.png +0 -0
- package/preview/assets/images/nebular.medallion.transparent..png +0 -0
- package/preview/assets/images/testcard.png +0 -0
- package/preview/assets/webfonts/fa-brands-400.woff2 +0 -0
- package/preview/assets/webfonts/fa-solid-900.woff2 +0 -0
- package/preview/bundle.min.js +6 -0
- package/preview/index.css +28 -0
- package/preview/index.html +23 -0
- package/readme.md +132 -0
- package/skill/nebular-overlay/SKILL.md +56 -0
- package/skill/nebular-overlay/agents/openai.yaml +4 -0
- package/skill/nebular-overlay/references/examples.md +327 -0
- package/skill/nebular-overlay/references/manifest.md +153 -0
- package/skill/nebular-overlay/references/runtime-and-design.md +88 -0
- package/skill/nebular-overlay/scripts/validate-templates.mjs +244 -0
- package/templates/all/broadcast/breaking-news/index.html +100 -0
- package/templates/all/broadcast/breaking-news/manifest.json +9 -0
- package/templates/all/broadcast/empty/index.html +46 -0
- package/templates/all/broadcast/empty/manifest.json +7 -0
- package/templates/all/broadcast/empty/nebular.medallion.transparent.png +0 -0
- package/templates/all/broadcast/event-countdown/index.html +160 -0
- package/templates/all/broadcast/event-countdown/logo.png +0 -0
- package/templates/all/broadcast/event-countdown/manifest.json +33 -0
- package/templates/all/broadcast/lower-third/index.html +77 -0
- package/templates/all/broadcast/lower-third/manifest.json +8 -0
- package/templates/all/broadcast/newsticker/index.html +153 -0
- package/templates/all/broadcast/newsticker/manifest.json +21 -0
- package/templates/all/broadcast/newsticker-frames/background-label-green.png +0 -0
- package/templates/all/broadcast/newsticker-frames/background-label-orange.png +0 -0
- package/templates/all/broadcast/newsticker-frames/banner.png +0 -0
- package/templates/all/broadcast/newsticker-frames/index.html +229 -0
- package/templates/all/broadcast/newsticker-frames/manifest.json +20 -0
- package/templates/all/broadcast/weather/index.html +382 -0
- package/templates/all/broadcast/weather/manifest.json +104 -0
- package/templates/all/engagement/call-to-action/index.html +116 -0
- package/templates/all/engagement/call-to-action/manifest.json +12 -0
- package/templates/all/engagement/chat/index.html +121 -0
- package/templates/all/engagement/chat/manifest.json +32 -0
- package/templates/all/engagement/post/index.html +132 -0
- package/templates/all/engagement/post/manifest.json +31 -0
- package/templates/all/gambling/bingo/index.html +227 -0
- package/templates/all/gambling/bingo/manifest.json +144 -0
- package/templates/all/gambling/keno/index.html +202 -0
- package/templates/all/gambling/keno/manifest.json +129 -0
- package/templates/all/gambling/lottery/index.html +159 -0
- package/templates/all/gambling/lottery/manifest.json +95 -0
- package/templates/all/gambling/prize-wall/index.html +256 -0
- package/templates/all/gambling/prize-wall/manifest.json +108 -0
- package/templates/all/gambling/roulette/index.html +200 -0
- package/templates/all/gambling/roulette/manifest.json +78 -0
- package/templates/all/gaming/battleship/index.html +578 -0
- package/templates/all/gaming/battleship/manifest.json +279 -0
- package/templates/all/gaming/gta/mission/index.html +121 -0
- package/templates/all/gaming/gta/mission/manifest.json +38 -0
- package/templates/all/sports/football/scoreboard/index.html +115 -0
- package/templates/all/sports/football/scoreboard/manifest.json +54 -0
- package/templates/all/sports/tennis/scoreboard/index.html +120 -0
- package/templates/all/sports/tennis/scoreboard/manifest.json +92 -0
- package/templates/all/trivia/audience-poll/index.html +60 -0
- package/templates/all/trivia/audience-poll/manifest.json +26 -0
- package/templates/all/trivia/classic-quiz/index.html +49 -0
- package/templates/all/trivia/classic-quiz/manifest.json +15 -0
- package/templates/all/trivia/lightning-round/index.html +126 -0
- package/templates/all/trivia/lightning-round/manifest.json +27 -0
- package/templates/all/trivia/pasapalabra/index.html +249 -0
- package/templates/all/trivia/pasapalabra/manifest.json +68 -0
- package/templates/all/trivia/quiz-deck/index.html +58 -0
- package/templates/all/trivia/quiz-deck/manifest.json +25 -0
- package/templates/all/trivia/true-or-false/index.html +81 -0
- package/templates/all/trivia/true-or-false/manifest.json +27 -0
- package/templates/basic/broadcast/empty/index.html +46 -0
- package/templates/basic/broadcast/empty/manifest.json +7 -0
- package/templates/basic/broadcast/empty/nebular.medallion.transparent.png +0 -0
- package/templates/basic/broadcast/event-countdown/index.html +160 -0
- package/templates/basic/broadcast/event-countdown/logo.png +0 -0
- package/templates/basic/broadcast/event-countdown/manifest.json +33 -0
- package/templates/basic/broadcast/lower-third/index.html +77 -0
- package/templates/basic/broadcast/lower-third/manifest.json +8 -0
- package/templates/basic/broadcast/newsticker/index.html +153 -0
- package/templates/basic/broadcast/newsticker/manifest.json +21 -0
- package/templates/basic/broadcast/weather/index.html +382 -0
- package/templates/basic/broadcast/weather/manifest.json +104 -0
- package/templates/basic/engagement/call-to-action/index.html +116 -0
- package/templates/basic/engagement/call-to-action/manifest.json +12 -0
- package/templates/basic/engagement/chat/index.html +121 -0
- package/templates/basic/engagement/chat/manifest.json +32 -0
- package/templates/basic/engagement/post/index.html +132 -0
- package/templates/basic/engagement/post/manifest.json +31 -0
- package/templates/basic/sports/football/scoreboard/index.html +115 -0
- package/templates/basic/sports/football/scoreboard/manifest.json +54 -0
- package/templates/basic/sports/tennis/scoreboard/index.html +120 -0
- package/templates/basic/sports/tennis/scoreboard/manifest.json +92 -0
- package/templates/debug/testing/security-probe/forbidden.js +1 -0
- package/templates/debug/testing/security-probe/forbidden.json +1 -0
- package/templates/debug/testing/security-probe/forbidden.mp4 +1 -0
- package/templates/debug/testing/security-probe/forbidden.svg +1 -0
- package/templates/debug/testing/security-probe/index.html +367 -0
- package/templates/debug/testing/security-probe/manifest.json +13 -0
- package/templates/empty/broadcast/empty/index.html +46 -0
- package/templates/empty/broadcast/empty/manifest.json +7 -0
- package/templates/empty/broadcast/empty/nebular.medallion.transparent.png +0 -0
- package/templates/games/gambling/bingo/index.html +227 -0
- package/templates/games/gambling/bingo/manifest.json +56 -0
- package/templates/games/gambling/keno/index.html +202 -0
- package/templates/games/gambling/keno/manifest.json +129 -0
- package/templates/games/gambling/lottery/index.html +159 -0
- package/templates/games/gambling/lottery/manifest.json +95 -0
- package/templates/games/gambling/prize-wall/index.html +256 -0
- package/templates/games/gambling/prize-wall/manifest.json +108 -0
- package/templates/games/gambling/roulette/index.html +200 -0
- package/templates/games/gambling/roulette/manifest.json +78 -0
- package/templates/games/gaming/battleship/index.html +578 -0
- package/templates/games/gaming/battleship/manifest.json +279 -0
- package/templates/games/gaming/gta/mission/index.html +121 -0
- package/templates/games/gaming/gta/mission/manifest.json +38 -0
- package/templates/games/trivia/audience-poll/index.html +60 -0
- package/templates/games/trivia/audience-poll/manifest.json +26 -0
- package/templates/games/trivia/classic-quiz/index.html +49 -0
- package/templates/games/trivia/classic-quiz/manifest.json +15 -0
- package/templates/games/trivia/lightning-round/index.html +126 -0
- package/templates/games/trivia/lightning-round/manifest.json +27 -0
- package/templates/games/trivia/pasapalabra/index.html +249 -0
- package/templates/games/trivia/pasapalabra/manifest.json +68 -0
- package/templates/games/trivia/quiz-deck/index.html +58 -0
- package/templates/games/trivia/quiz-deck/manifest.json +25 -0
- package/templates/games/trivia/true-or-false/index.html +81 -0
- package/templates/games/trivia/true-or-false/manifest.json +27 -0
- package/tool/cli.cjs +7 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
throw new Error("External JavaScript was executed");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"warning":"This unsupported asset must not be served."}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This is deliberately not a real video. The resource type must be rejected before its contents matter.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><rect width="32" height="32" fill="red"/></svg>
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>Overlay security probe</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; font-family: Inter, system-ui, sans-serif; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
body { display: flex; flex-direction: column; height: 100vh; margin: 0; overflow: hidden; padding: 1.25vh 1vw; color: #eef5ff; }
|
|
11
|
+
header { display: flex; flex: none; align-items: end; justify-content: space-between; gap: 2rem; margin-bottom: .8vh; }
|
|
12
|
+
h1 { margin: 0; font-size: 2.6vh; letter-spacing: .04em; }
|
|
13
|
+
p { margin: .25vh 0 0; color: #9fb0c4; font-size: 1.2vh; }
|
|
14
|
+
#summary { font: 700 1.3vh ui-monospace, monospace; white-space: nowrap; }
|
|
15
|
+
#results { display: grid; flex: 1; min-height: 0; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .3vh .55vw; }
|
|
16
|
+
.probe { min-width: 0; min-height: 0; overflow: hidden; padding: .25vh .55vw; border: 1px solid #26384c; border-radius: .38rem; background: #101c2a; }
|
|
17
|
+
.probe strong { display: block; overflow: hidden; font-size: 1.18vh; line-height: 1.25; text-overflow: ellipsis; white-space: nowrap; }
|
|
18
|
+
.probe span { display: block; margin-top: .08vh; overflow: hidden; color: #9fb0c4; font: 1vh/1.2 ui-monospace, monospace; text-overflow: ellipsis; white-space: nowrap; }
|
|
19
|
+
.blocked { border-color: #168b63; background: #0d2a25; }
|
|
20
|
+
.blocked strong { color: #57e6ad; }
|
|
21
|
+
.exposed { border-color: #d64b55; background: #35171e; }
|
|
22
|
+
.exposed strong { color: #ff8790; }
|
|
23
|
+
.policy { border-color: #d49332; background: #342814; }
|
|
24
|
+
.policy strong { color: #ffc76f; }
|
|
25
|
+
.inconclusive { border-color: #637083; }
|
|
26
|
+
.inconclusive strong { color: #bec8d5; }
|
|
27
|
+
.pending { opacity: .72; }
|
|
28
|
+
footer { flex: none; margin-top: .5vh; color: #74869a; font-size: .9vh; }
|
|
29
|
+
</style>
|
|
30
|
+
<script>
|
|
31
|
+
(() => {
|
|
32
|
+
const definitions = [];
|
|
33
|
+
let generation = 0;
|
|
34
|
+
|
|
35
|
+
function add(name, group, execute, forbidden = true) {
|
|
36
|
+
definitions.push({name, group, execute, forbidden});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function message(error) {
|
|
40
|
+
return String(error?.message || error || "rejected").replace(/\s+/g, " ").slice(0, 110);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function outcome(definition, result) {
|
|
44
|
+
if (result.state) return result;
|
|
45
|
+
const {allowed, detail} = result;
|
|
46
|
+
const expected = definition.forbidden;
|
|
47
|
+
return {
|
|
48
|
+
state: allowed === null ? "inconclusive" : (allowed === expected ? "exposed" : "blocked"),
|
|
49
|
+
detail
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function policyAllows(feature, fallback) {
|
|
54
|
+
const policy = document.permissionsPolicy || document.featurePolicy;
|
|
55
|
+
if (policy?.allowsFeature) {
|
|
56
|
+
try {
|
|
57
|
+
const allowed = policy.allowsFeature(feature);
|
|
58
|
+
return {state: allowed ? "policy" : "blocked", detail: `Permissions Policy ${allowed ? "allows" : "blocks"} ${feature}`};
|
|
59
|
+
} catch (error) { return {state: "inconclusive", detail: message(error)}; }
|
|
60
|
+
}
|
|
61
|
+
return {state: "inconclusive", detail: `Permissions Policy unavailable; API ${fallback ? "present" : "absent"}`};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function cspAttempt(directive, attempt, timeout = 100) {
|
|
65
|
+
let violation = null;
|
|
66
|
+
const onViolation = event => {
|
|
67
|
+
if (event.effectiveDirective === directive) violation = event;
|
|
68
|
+
};
|
|
69
|
+
document.addEventListener("securitypolicyviolation", onViolation);
|
|
70
|
+
let attempted;
|
|
71
|
+
try {
|
|
72
|
+
attempted = await Promise.race([
|
|
73
|
+
Promise.resolve().then(attempt),
|
|
74
|
+
new Promise(resolve => setTimeout(() => resolve({detail: "operation timed out after policy check"}), 1500))
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
catch (error) { attempted = {detail: message(error)}; }
|
|
78
|
+
await new Promise(resolve => setTimeout(resolve, timeout));
|
|
79
|
+
document.removeEventListener("securitypolicyviolation", onViolation);
|
|
80
|
+
if (violation) {
|
|
81
|
+
return {allowed: false, detail: `CSP ${violation.effectiveDirective} blocked ${violation.blockedURI || "request"}`};
|
|
82
|
+
}
|
|
83
|
+
if (attempted?.allowed === null) {
|
|
84
|
+
return {state: "inconclusive", detail: attempted.detail || "operation produced no verifiable result"};
|
|
85
|
+
}
|
|
86
|
+
return {allowed: true, detail: attempted?.detail || "no matching CSP violation"};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function executeProbe(definition, timeout = 2500) {
|
|
90
|
+
return Promise.race([
|
|
91
|
+
Promise.resolve().then(definition.execute),
|
|
92
|
+
new Promise(resolve => setTimeout(() => resolve({
|
|
93
|
+
state: "inconclusive",
|
|
94
|
+
detail: `probe did not finish within ${timeout} ms`
|
|
95
|
+
}), timeout))
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resource(tag, attributes, timeout = 1200) {
|
|
100
|
+
return new Promise(resolve => {
|
|
101
|
+
const element = document.createElement(tag);
|
|
102
|
+
Object.assign(element, attributes);
|
|
103
|
+
element.hidden = true;
|
|
104
|
+
element.onload = () => { element.remove(); resolve({allowed: true, detail: "resource loaded"}); };
|
|
105
|
+
element.onerror = () => { element.remove(); resolve({allowed: false, detail: "load rejected"}); };
|
|
106
|
+
document.body.appendChild(element);
|
|
107
|
+
setTimeout(() => { element.remove(); resolve({allowed: null, detail: "no load/error event"}); }, timeout);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function connection(factory, timeout = 800) {
|
|
112
|
+
return new Promise(resolve => {
|
|
113
|
+
let object;
|
|
114
|
+
try { object = factory(); }
|
|
115
|
+
catch (error) { resolve({allowed: false, detail: message(error)}); return; }
|
|
116
|
+
object.onopen = () => { object.close?.(); resolve({allowed: true, detail: "connection opened"}); };
|
|
117
|
+
object.onerror = () => { object.close?.(); resolve({detail: "connection failed after policy check"}); };
|
|
118
|
+
setTimeout(() => { object.close?.(); resolve({detail: "connection did not open"}); }, timeout);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
add("fetch external", "Network", () => cspAttempt("connect-src", async () => {
|
|
123
|
+
try { await fetch("https://example.invalid/overlay-probe"); return {detail: "request completed"}; }
|
|
124
|
+
catch (error) { return {detail: `network failed: ${message(error)}`}; }
|
|
125
|
+
}));
|
|
126
|
+
add("fetch loopback", "Network", () => cspAttempt("connect-src", async () => {
|
|
127
|
+
try { await fetch("http://127.0.0.1:1/overlay-probe"); return {detail: "request completed"}; }
|
|
128
|
+
catch (error) { return {detail: `network failed: ${message(error)}`}; }
|
|
129
|
+
}));
|
|
130
|
+
add("XMLHttpRequest", "Network", () => cspAttempt("connect-src", () => new Promise(resolve => {
|
|
131
|
+
const xhr = new XMLHttpRequest();
|
|
132
|
+
xhr.onload = () => resolve({detail: "request completed"});
|
|
133
|
+
xhr.onerror = () => resolve({detail: "network failed after policy check"});
|
|
134
|
+
try { xhr.open("GET", "https://example.invalid/overlay-probe"); xhr.send(); }
|
|
135
|
+
catch (error) { resolve({detail: message(error)}); }
|
|
136
|
+
})));
|
|
137
|
+
add("WebSocket", "Network", () => cspAttempt("connect-src", () => connection(() => new WebSocket("wss://example.invalid/overlay-probe"))));
|
|
138
|
+
add("EventSource", "Network", () => cspAttempt("connect-src", () => connection(() => new EventSource("https://example.invalid/overlay-probe"))));
|
|
139
|
+
add("sendBeacon", "Network", () => cspAttempt("connect-src", () => ({detail: `sendBeacon returned ${navigator.sendBeacon("https://example.invalid/overlay-probe", "probe")}`})));
|
|
140
|
+
add("external image", "Resources", () => cspAttempt("img-src", () => resource("img", {src: "https://example.invalid/probe.png"})));
|
|
141
|
+
add("file:// filesystem", "Resources", () => resource("img", {src: "file:///etc/passwd"}));
|
|
142
|
+
add("encoded traversal", "Resources", () => resource("img", {src: "./%2e%2e%2f%2e%2e%2fetc%2fpasswd"}));
|
|
143
|
+
add("unsupported SVG", "Resources", () => resource("link", {rel: "stylesheet", href: "forbidden.svg"}));
|
|
144
|
+
add("external script file", "Resources", () => resource("script", {src: "forbidden.js"}));
|
|
145
|
+
add("unsupported JS route", "Resources", () => resource("link", {rel: "stylesheet", href: "forbidden.js"}));
|
|
146
|
+
add("unsupported JSON route", "Resources", () => resource("link", {rel: "stylesheet", href: "forbidden.json"}));
|
|
147
|
+
add("unsupported MP4 route", "Resources", () => resource("link", {rel: "stylesheet", href: "forbidden.mp4"}));
|
|
148
|
+
add("audio resource", "Resources", () => resource("audio", {src: "forbidden.mp4"}));
|
|
149
|
+
add("video resource", "Resources", () => resource("video", {src: "forbidden.mp4"}));
|
|
150
|
+
|
|
151
|
+
add("iframe", "Contexts", () => new Promise(resolve => {
|
|
152
|
+
const frame = document.createElement("iframe");
|
|
153
|
+
const token = `frame-${Date.now()}-${Math.random()}`;
|
|
154
|
+
let finished = false;
|
|
155
|
+
let timer;
|
|
156
|
+
const finish = result => {
|
|
157
|
+
if (finished) return;
|
|
158
|
+
finished = true;
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
window.removeEventListener("message", onMessage);
|
|
161
|
+
document.removeEventListener("securitypolicyviolation", onViolation);
|
|
162
|
+
frame.remove();
|
|
163
|
+
resolve(result);
|
|
164
|
+
};
|
|
165
|
+
const onMessage = event => {
|
|
166
|
+
if (event.source === frame.contentWindow && event.data === token) {
|
|
167
|
+
finish({allowed: true, detail: "navigated frame executed and messaged its parent"});
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const onViolation = event => {
|
|
171
|
+
if (event.effectiveDirective === "frame-src") {
|
|
172
|
+
finish({allowed: false, detail: `CSP frame-src blocked ${event.blockedURI || "frame"}`});
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const source = `<script>parent.postMessage(${JSON.stringify(token)}, "*")<` + "/script>";
|
|
176
|
+
frame.hidden = true;
|
|
177
|
+
frame.src = `data:text/html;base64,${btoa(source)}`;
|
|
178
|
+
window.addEventListener("message", onMessage);
|
|
179
|
+
document.addEventListener("securitypolicyviolation", onViolation);
|
|
180
|
+
document.body.appendChild(frame);
|
|
181
|
+
timer = setTimeout(() => finish({allowed: null, detail: "no execution message or CSP violation"}), 800);
|
|
182
|
+
}));
|
|
183
|
+
add("embedded object", "Contexts", () => cspAttempt("object-src", () => resource("object", {data: "forbidden.svg", type: "image/svg+xml"})));
|
|
184
|
+
add("popup window", "Contexts", () => new Promise(resolve => {
|
|
185
|
+
const popup = window.open("about:blank", "_blank", "width=100,height=100");
|
|
186
|
+
if (!popup) { resolve({allowed: false, detail: "window.open returned null"}); return; }
|
|
187
|
+
setTimeout(() => {
|
|
188
|
+
if (popup.closed) resolve({allowed: false, detail: "renderer closed the popup"});
|
|
189
|
+
else { popup.close(); resolve({allowed: true, detail: "popup remained open"}); }
|
|
190
|
+
}, 150);
|
|
191
|
+
}));
|
|
192
|
+
add("Web Worker", "Execution", () => new Promise(resolve => {
|
|
193
|
+
let finished = false;
|
|
194
|
+
let timer;
|
|
195
|
+
let url;
|
|
196
|
+
const finish = result => {
|
|
197
|
+
if (finished) return;
|
|
198
|
+
finished = true;
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
if (url) URL.revokeObjectURL(url);
|
|
201
|
+
resolve(result);
|
|
202
|
+
};
|
|
203
|
+
try {
|
|
204
|
+
// A successful worker shuts itself down; host-side terminate() is
|
|
205
|
+
// deliberately avoided because hardened Chromium can block there.
|
|
206
|
+
url = URL.createObjectURL(new Blob(["postMessage('executed');close()"], {type: "text/javascript"}));
|
|
207
|
+
const worker = new Worker(url);
|
|
208
|
+
worker.onmessage = event => finish({allowed: event.data === "executed", detail: "worker code posted a message"});
|
|
209
|
+
worker.onerror = event => finish({allowed: false, detail: event.message || "worker execution rejected"});
|
|
210
|
+
timer = setTimeout(() => finish({allowed: null, detail: "worker created but did not execute"}), 800);
|
|
211
|
+
} catch (error) { finish({allowed: false, detail: message(error)}); }
|
|
212
|
+
}));
|
|
213
|
+
add("Shared Worker", "Execution", () => new Promise(resolve => {
|
|
214
|
+
if (!("SharedWorker" in window)) { resolve({allowed: null, detail: "API unavailable"}); return; }
|
|
215
|
+
let finished = false;
|
|
216
|
+
let timer;
|
|
217
|
+
let url;
|
|
218
|
+
const finish = result => {
|
|
219
|
+
if (finished) return;
|
|
220
|
+
finished = true;
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
if (url) URL.revokeObjectURL(url);
|
|
223
|
+
resolve(result);
|
|
224
|
+
};
|
|
225
|
+
try {
|
|
226
|
+
url = URL.createObjectURL(new Blob(["onconnect=e=>{e.ports[0].postMessage('executed');e.ports[0].close();close()}"], {type: "text/javascript"}));
|
|
227
|
+
const worker = new SharedWorker(url);
|
|
228
|
+
worker.port.onmessage = event => finish({allowed: event.data === "executed", detail: "shared worker code posted a message"});
|
|
229
|
+
worker.onerror = event => finish({allowed: false, detail: event.message || "shared worker execution rejected"});
|
|
230
|
+
worker.port.start();
|
|
231
|
+
timer = setTimeout(() => finish({allowed: null, detail: "worker created but did not execute"}), 800);
|
|
232
|
+
} catch (error) { finish({allowed: false, detail: message(error)}); }
|
|
233
|
+
}));
|
|
234
|
+
add("eval", "Execution", () => {
|
|
235
|
+
try { return {allowed: eval("true"), detail: "eval executed"}; }
|
|
236
|
+
catch (error) { return {allowed: false, detail: message(error)}; }
|
|
237
|
+
});
|
|
238
|
+
add("Function constructor", "Execution", () => {
|
|
239
|
+
try { return {allowed: Function("return true")(), detail: "function compiled"}; }
|
|
240
|
+
catch (error) { return {allowed: false, detail: message(error)}; }
|
|
241
|
+
});
|
|
242
|
+
add("WebAssembly", "Execution", async () => {
|
|
243
|
+
try { await WebAssembly.compile(new Uint8Array([0,97,115,109,1,0,0,0])); return {allowed: true, detail: "module compiled"}; }
|
|
244
|
+
catch (error) { return {allowed: false, detail: message(error)}; }
|
|
245
|
+
});
|
|
246
|
+
add("Canvas 2D", "Rendering", () => ({allowed: Boolean(document.createElement("canvas").getContext("2d")), detail: "context creation"}));
|
|
247
|
+
add("WebGL", "Rendering", () => {
|
|
248
|
+
const canvas = document.createElement("canvas");
|
|
249
|
+
return {allowed: Boolean(canvas.getContext("webgl2") || canvas.getContext("webgl")), detail: "context creation"};
|
|
250
|
+
});
|
|
251
|
+
add("WebAudio", "Rendering", async () => {
|
|
252
|
+
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
253
|
+
if (!AudioContext) return {allowed: false, detail: "AudioContext constructors unavailable"};
|
|
254
|
+
try { const context = new AudioContext(); await context.close(); return {allowed: true, detail: "audio context created"}; }
|
|
255
|
+
catch (error) { return {allowed: false, detail: message(error)}; }
|
|
256
|
+
});
|
|
257
|
+
add("localStorage", "Persistence", () => {
|
|
258
|
+
try { localStorage.setItem("overlay-security-probe", "1"); localStorage.removeItem("overlay-security-probe"); return {allowed: true, detail: "read/write succeeded"}; }
|
|
259
|
+
catch (error) { return {allowed: false, detail: message(error)}; }
|
|
260
|
+
});
|
|
261
|
+
add("sessionStorage", "Persistence", () => {
|
|
262
|
+
try { sessionStorage.setItem("overlay-security-probe", "1"); sessionStorage.removeItem("overlay-security-probe"); return {allowed: true, detail: "read/write succeeded"}; }
|
|
263
|
+
catch (error) { return {allowed: false, detail: message(error)}; }
|
|
264
|
+
});
|
|
265
|
+
add("Cache Storage", "Persistence", async () => {
|
|
266
|
+
if (!("caches" in window)) return {allowed: null, detail: "API unavailable"};
|
|
267
|
+
try { await caches.open("overlay-security-probe"); await caches.delete("overlay-security-probe"); return {allowed: true, detail: "cache opened"}; }
|
|
268
|
+
catch (error) { return {allowed: false, detail: message(error)}; }
|
|
269
|
+
});
|
|
270
|
+
add("file picker", "Devices", async () => {
|
|
271
|
+
return "showOpenFilePicker" in window
|
|
272
|
+
? {state: "inconclusive", detail: "API present; not invoked without a user gesture"}
|
|
273
|
+
: {state: "blocked", detail: "API unavailable"};
|
|
274
|
+
});
|
|
275
|
+
add("camera", "Devices", () => policyAllows("camera", Boolean(navigator.mediaDevices?.getUserMedia)));
|
|
276
|
+
add("microphone", "Devices", () => policyAllows("microphone", Boolean(navigator.mediaDevices?.getUserMedia)));
|
|
277
|
+
add("screen capture", "Devices", () => policyAllows("display-capture", Boolean(navigator.mediaDevices?.getDisplayMedia)));
|
|
278
|
+
add("geolocation", "Devices", () => policyAllows("geolocation", Boolean(navigator.geolocation)));
|
|
279
|
+
add("USB", "Devices", () => policyAllows("usb", Boolean(navigator.usb)));
|
|
280
|
+
add("serial", "Devices", () => policyAllows("serial", Boolean(navigator.serial)));
|
|
281
|
+
add("HID", "Devices", () => policyAllows("hid", Boolean(navigator.hid)));
|
|
282
|
+
add("clipboard read", "Devices", () => policyAllows("clipboard-read", Boolean(navigator.clipboard?.read)));
|
|
283
|
+
|
|
284
|
+
/*
|
|
285
|
+
add("IndexedDB", "Persistence", () => new Promise(resolve => {
|
|
286
|
+
let finished = false;
|
|
287
|
+
const finish = result => {
|
|
288
|
+
if (finished) return;
|
|
289
|
+
finished = true;
|
|
290
|
+
clearTimeout(timer);
|
|
291
|
+
resolve(result);
|
|
292
|
+
};
|
|
293
|
+
const timer = setTimeout(() => finish({allowed: null, detail: "IndexedDB produced no event within 1200 ms"}), 1200);
|
|
294
|
+
try {
|
|
295
|
+
const request = indexedDB.open("overlay-security-probe");
|
|
296
|
+
request.onsuccess = () => {
|
|
297
|
+
request.result.close();
|
|
298
|
+
indexedDB.deleteDatabase("overlay-security-probe");
|
|
299
|
+
finish({allowed: true, detail: "database opened"});
|
|
300
|
+
};
|
|
301
|
+
request.onerror = () => finish({allowed: false, detail: message(request.error)});
|
|
302
|
+
request.onblocked = () => finish({allowed: null, detail: "database open was blocked"});
|
|
303
|
+
} catch (error) { finish({allowed: false, detail: message(error)}); }
|
|
304
|
+
}));
|
|
305
|
+
*/
|
|
306
|
+
|
|
307
|
+
async function run() {
|
|
308
|
+
const currentGeneration = ++generation;
|
|
309
|
+
const resultRoot = document.getElementById("results");
|
|
310
|
+
resultRoot.style.gridTemplateRows = `repeat(${Math.ceil(definitions.length / 2)}, minmax(0, 1fr))`;
|
|
311
|
+
resultRoot.replaceChildren(...definitions.map((definition, index) => {
|
|
312
|
+
const row = document.createElement("div");
|
|
313
|
+
row.className = "probe pending";
|
|
314
|
+
row.id = `probe-${index}`;
|
|
315
|
+
const title = document.createElement("strong");
|
|
316
|
+
title.textContent = `${definition.group} · ${definition.name}`;
|
|
317
|
+
const detail = document.createElement("span");
|
|
318
|
+
detail.textContent = "testing…";
|
|
319
|
+
row.append(title, detail);
|
|
320
|
+
return row;
|
|
321
|
+
}));
|
|
322
|
+
|
|
323
|
+
const counts = {blocked: 0, exposed: 0, policy: 0, inconclusive: 0};
|
|
324
|
+
// Worker construction touches a separate Chromium process. Run those
|
|
325
|
+
// probes last so even a browser-level worker defect cannot hide the
|
|
326
|
+
// results of the rest of the suite.
|
|
327
|
+
const executionOrder = [...definitions.entries()].sort(([, left], [, right]) => {
|
|
328
|
+
const priority = definition => definition.name === "Web Worker" ? 2
|
|
329
|
+
: definition.name === "Shared Worker" ? 1
|
|
330
|
+
: 0;
|
|
331
|
+
return priority(left) - priority(right);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
for (const [index, definition] of executionOrder) {
|
|
335
|
+
let result;
|
|
336
|
+
try {
|
|
337
|
+
const raw = await executeProbe(definition);
|
|
338
|
+
result = outcome(definition, raw);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
result = outcome(definition, {allowed: false, detail: message(error)});
|
|
341
|
+
}
|
|
342
|
+
if (generation !== currentGeneration) return;
|
|
343
|
+
const row = document.getElementById(`probe-${index}`);
|
|
344
|
+
row.className = `probe ${result.state}`;
|
|
345
|
+
row.lastElementChild.textContent = `${result.state.toUpperCase()} · ${result.detail}`;
|
|
346
|
+
counts[result.state]++;
|
|
347
|
+
document.getElementById("summary").textContent = `${counts.blocked} blocked · ${counts.exposed} exposed · ${counts.policy} policy allowed · ${counts.inconclusive} inconclusive`;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
window.addEventListener("nebular-overlay:update", ({detail}) => {
|
|
352
|
+
if (detail?.control?.run) run();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
})();
|
|
356
|
+
|
|
357
|
+
</script>
|
|
358
|
+
</head>
|
|
359
|
+
<body>
|
|
360
|
+
<header>
|
|
361
|
+
<div><h1>Overlay Security Probe</h1><p>Green: blocked · Red: executed · Amber: policy allows access · Grey: inconclusive</p></div>
|
|
362
|
+
<div id="summary">starting…</div>
|
|
363
|
+
</header>
|
|
364
|
+
<main id="results"></main>
|
|
365
|
+
<footer>Device probes inspect Permissions Policy without requesting user permission. Constructors alone never count as successful execution.</footer>
|
|
366
|
+
</body>
|
|
367
|
+
</html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"instructions": "Security regression template. It deliberately attempts forbidden browser and template-resource capabilities and reports whether each boundary is blocked. This template is for operator testing only and must not be used as a production overlay.",
|
|
3
|
+
"meta": {
|
|
4
|
+
"control/run": {
|
|
5
|
+
"type": "action",
|
|
6
|
+
"phase": "play",
|
|
7
|
+
"icon": "undo",
|
|
8
|
+
"text": "Run security probes",
|
|
9
|
+
"hint": "Run the complete security-policy test suite",
|
|
10
|
+
"default": 1
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
<head>
|
|
3
|
+
<style>
|
|
4
|
+
body {
|
|
5
|
+
font-family: sans-serif;
|
|
6
|
+
margin: 1.5rem;
|
|
7
|
+
color: white;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
h1 {
|
|
11
|
+
font-weight: bold;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#logo {
|
|
15
|
+
position: absolute;
|
|
16
|
+
right: 1rem;
|
|
17
|
+
top: 1rem;
|
|
18
|
+
display: none;
|
|
19
|
+
width: 10rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#logo.on {
|
|
23
|
+
display: block;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
</style>
|
|
27
|
+
<script>
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
window.addEventListener("nebular-overlay:update", ({detail}) => {
|
|
31
|
+
|
|
32
|
+
const
|
|
33
|
+
programElement = document.getElementById("program"),
|
|
34
|
+
logoElement = document.getElementById("logo");
|
|
35
|
+
|
|
36
|
+
if (typeof detail.program === "string") programElement.textContent = detail.program;
|
|
37
|
+
if (detail.logo) logoElement.classList.toggle("on", detail.logo === "on");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
</script>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
<h1 id="program"></h1>
|
|
44
|
+
<img id="logo" src="nebular.medallion.transparent.png">
|
|
45
|
+
</body>
|
|
46
|
+
</html>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"instructions": "Empty overlay with the program name",
|
|
3
|
+
"meta": {
|
|
4
|
+
"program": {"type": "string", "default": "", "hint": "Type the program name", "label": "Program Name"},
|
|
5
|
+
"logo": {"type": "choose", "default": "off", "hint": "Logo ON/OFF", "label": "Logo Visibility", "choose": ["on", "off"]}
|
|
6
|
+
}
|
|
7
|
+
}
|