skill-sentry-cli 2.0.0__py3-none-any.whl
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.
- skill_sentry_cli/__init__.py +5 -0
- skill_sentry_cli/__main__.py +4 -0
- skill_sentry_cli/cli.py +918 -0
- skill_sentry_cli/core/__init__.py +1 -0
- skill_sentry_cli/core/anchors/__init__.py +5 -0
- skill_sentry_cli/core/anchors/credential_flow.py +440 -0
- skill_sentry_cli/core/anchors/declared_hosts.py +78 -0
- skill_sentry_cli/core/cache/__init__.py +35 -0
- skill_sentry_cli/core/cache/hash_utils.py +206 -0
- skill_sentry_cli/core/cache/sqlite_store.py +925 -0
- skill_sentry_cli/core/config.py +299 -0
- skill_sentry_cli/core/contracts.py +551 -0
- skill_sentry_cli/core/decisions/__init__.py +44 -0
- skill_sentry_cli/core/decisions/base.py +174 -0
- skill_sentry_cli/core/decisions/calibration.py +325 -0
- skill_sentry_cli/core/decisions/laya.py +439 -0
- skill_sentry_cli/core/decisions/mapping.py +218 -0
- skill_sentry_cli/core/decisions/mock.py +118 -0
- skill_sentry_cli/core/decisions/questions.py +118 -0
- skill_sentry_cli/core/decisions/report.py +65 -0
- skill_sentry_cli/core/decisions/selector.py +151 -0
- skill_sentry_cli/core/engine.py +1412 -0
- skill_sentry_cli/core/evasion/__init__.py +27 -0
- skill_sentry_cli/core/evasion/detector.py +210 -0
- skill_sentry_cli/core/exit_codes.py +13 -0
- skill_sentry_cli/core/models/__init__.py +20 -0
- skill_sentry_cli/core/models/_json.py +147 -0
- skill_sentry_cli/core/models/base.py +50 -0
- skill_sentry_cli/core/models/gemma_cloud.py +240 -0
- skill_sentry_cli/core/models/gemma_local.py +273 -0
- skill_sentry_cli/core/models/image_utils.py +112 -0
- skill_sentry_cli/core/models/mock.py +152 -0
- skill_sentry_cli/core/models/selector.py +123 -0
- skill_sentry_cli/core/parser/__init__.py +29 -0
- skill_sentry_cli/core/parser/skill_parser.py +520 -0
- skill_sentry_cli/core/parser/typosquat.py +167 -0
- skill_sentry_cli/core/poisoned_well/__init__.py +52 -0
- skill_sentry_cli/core/poisoned_well/attacker_profile.py +230 -0
- skill_sentry_cli/core/poisoned_well/canary_gen.py +413 -0
- skill_sentry_cli/core/poisoned_well/exfil_tracker.py +470 -0
- skill_sentry_cli/core/reports.py +114 -0
- skill_sentry_cli/core/sandbox/__init__.py +74 -0
- skill_sentry_cli/core/sandbox/assets/Dockerfile.sandbox +39 -0
- skill_sentry_cli/core/sandbox/assets/masquerade.c +484 -0
- skill_sentry_cli/core/sandbox/assets/run_skill.sh +41 -0
- skill_sentry_cli/core/sandbox/assets/sinkhole_sidecar.py +171 -0
- skill_sentry_cli/core/sandbox/canary_fs.py +108 -0
- skill_sentry_cli/core/sandbox/context_honeypot.py +97 -0
- skill_sentry_cli/core/sandbox/dns_sinkhole.py +218 -0
- skill_sentry_cli/core/sandbox/fake_http.py +194 -0
- skill_sentry_cli/core/sandbox/instructions.py +170 -0
- skill_sentry_cli/core/sandbox/masquerade.py +265 -0
- skill_sentry_cli/core/sandbox/monitor.py +278 -0
- skill_sentry_cli/core/sandbox/runner.py +776 -0
- skill_sentry_cli/core/sandbox/sidecar.py +327 -0
- skill_sentry_cli/core/semantic/__init__.py +9 -0
- skill_sentry_cli/core/semantic/analyzer.py +443 -0
- skill_sentry_cli/core/semantic/ast_anchor.py +371 -0
- skill_sentry_cli/core/semantic/ast_python.py +156 -0
- skill_sentry_cli/core/semantic/candidates.py +78 -0
- skill_sentry_cli/core/semantic/laya_analyzer.py +241 -0
- skill_sentry_cli/core/semantic/prompts.py +61 -0
- skill_sentry_cli/core/verdict/__init__.py +24 -0
- skill_sentry_cli/core/verdict/fusion.py +392 -0
- skill_sentry_cli/core/verdict/markdown_report.py +211 -0
- skill_sentry_cli/core/verdict/sarif_report.py +139 -0
- skill_sentry_cli/core/verdict/status.py +35 -0
- skill_sentry_cli/core/verdict/stix_report.py +132 -0
- skill_sentry_cli/core/version.py +38 -0
- skill_sentry_cli/core/vision/__init__.py +8 -0
- skill_sentry_cli/core/vision/image_extractor.py +91 -0
- skill_sentry_cli/core/vision/vision_scanner.py +274 -0
- skill_sentry_cli/data/__init__.py +1 -0
- skill_sentry_cli/data/known_skills_v1.json +276 -0
- skill_sentry_cli/data/sandbox_image.json +4 -0
- skill_sentry_cli/rules/credential_rules.toml +94 -0
- skill_sentry_cli/server/__init__.py +5 -0
- skill_sentry_cli/server/app.py +129 -0
- skill_sentry_cli/server/backend.py +63 -0
- skill_sentry_cli/server/demo_assets.py +62 -0
- skill_sentry_cli/server/demo_backend.py +475 -0
- skill_sentry_cli/server/demo_data.py +814 -0
- skill_sentry_cli/server/demo_mode.py +40 -0
- skill_sentry_cli/server/errors.py +48 -0
- skill_sentry_cli/server/live_backend.py +576 -0
- skill_sentry_cli/server/routes.py +191 -0
- skill_sentry_cli/skill/SKILL.md +67 -0
- skill_sentry_cli/skill/scripts/audit.sh +48 -0
- skill_sentry_cli/ui/dist/assets/index-Bf0h24IX.css +1 -0
- skill_sentry_cli/ui/dist/assets/index-CrYBKSSB.js +8 -0
- skill_sentry_cli/ui/dist/index.html +14 -0
- skill_sentry_cli-2.0.0.dist-info/METADATA +76 -0
- skill_sentry_cli-2.0.0.dist-info/RECORD +96 -0
- skill_sentry_cli-2.0.0.dist-info/WHEEL +4 -0
- skill_sentry_cli-2.0.0.dist-info/entry_points.txt +2 -0
- skill_sentry_cli-2.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SkillSentry sandbox masquerade + telemetry shim.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* 1. hide /.dockerenv from stat/access/open (the bind mount still exists),
|
|
6
|
+
* 2. redirect reads of /proc/{1,self}/cgroup and mountinfo to sanitised
|
|
7
|
+
* files generated by run_skill.sh,
|
|
8
|
+
* 3. append JSONL canary-read and probe events to the monitor log,
|
|
9
|
+
* 4. observe getenv() of canary values and probe command execution.
|
|
10
|
+
*
|
|
11
|
+
* It is intentionally small and defensive: no libc calls are made from the
|
|
12
|
+
* logging path (raw syscalls only) so a failed log write cannot recurse.
|
|
13
|
+
*
|
|
14
|
+
* Build (inside the sandbox image):
|
|
15
|
+
* gcc -shared -fPIC -O2 -Wall -Wextra -o masquerade.so masquerade.c -ldl
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
#define _GNU_SOURCE
|
|
19
|
+
#define _LARGEFILE64_SOURCE
|
|
20
|
+
#include <dlfcn.h>
|
|
21
|
+
#include <errno.h>
|
|
22
|
+
#include <fcntl.h>
|
|
23
|
+
#include <stdarg.h>
|
|
24
|
+
#include <stddef.h>
|
|
25
|
+
#include <stdio.h>
|
|
26
|
+
#include <stdlib.h>
|
|
27
|
+
#include <string.h>
|
|
28
|
+
#include <sys/stat.h>
|
|
29
|
+
#include <sys/syscall.h>
|
|
30
|
+
#include <sys/types.h>
|
|
31
|
+
#include <linux/stat.h>
|
|
32
|
+
#include <time.h>
|
|
33
|
+
#include <unistd.h>
|
|
34
|
+
|
|
35
|
+
#define LOG_PATH "/tmp/skillsentry/events.jsonl"
|
|
36
|
+
#define FAKE_CGROUP "/tmp/skillsentry/cgroup"
|
|
37
|
+
#define FAKE_MOUNTINFO "/tmp/skillsentry/mountinfo"
|
|
38
|
+
#define CANARY_ROOT "/home/sandbox/"
|
|
39
|
+
#define DOCKERENV "/.dockerenv"
|
|
40
|
+
|
|
41
|
+
static void json_escape(const char *src, char *dst, size_t cap) {
|
|
42
|
+
size_t out = 0;
|
|
43
|
+
if (cap == 0) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (src == NULL) {
|
|
47
|
+
dst[0] = '\0';
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
for (size_t i = 0; src[i] != '\0' && out + 7 < cap; i++) {
|
|
51
|
+
unsigned char c = (unsigned char)src[i];
|
|
52
|
+
switch (c) {
|
|
53
|
+
case '"': dst[out++] = '\\'; dst[out++] = '"'; break;
|
|
54
|
+
case '\\': dst[out++] = '\\'; dst[out++] = '\\'; break;
|
|
55
|
+
case '\n': dst[out++] = '\\'; dst[out++] = 'n'; break;
|
|
56
|
+
case '\r': dst[out++] = '\\'; dst[out++] = 'r'; break;
|
|
57
|
+
case '\t': dst[out++] = '\\'; dst[out++] = 't'; break;
|
|
58
|
+
default:
|
|
59
|
+
if (c < 0x20) {
|
|
60
|
+
int written = snprintf(dst + out, cap - out, "\\u%04x", c);
|
|
61
|
+
if (written > 0) {
|
|
62
|
+
out += (size_t)written;
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
dst[out++] = (char)c;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
dst[out] = '\0';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static int hex8(const char *value) {
|
|
73
|
+
if (value == NULL) {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
for (int i = 0; i < 8; i++) {
|
|
77
|
+
char c = value[i];
|
|
78
|
+
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
|
|
79
|
+
(c >= 'A' && c <= 'F'))) {
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static void marker_from(const char *text, char out[14]) {
|
|
87
|
+
out[0] = '\0';
|
|
88
|
+
if (text == NULL) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const char *cursor = text;
|
|
92
|
+
while ((cursor = strstr(cursor, "SKS")) != NULL) {
|
|
93
|
+
if (cursor[3] == '-' && hex8(cursor + 4)) {
|
|
94
|
+
snprintf(out, 14, "SKS-%.8s", cursor + 4);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (hex8(cursor + 3)) {
|
|
98
|
+
snprintf(out, 14, "SKS-%.8s", cursor + 3);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
cursor += 3;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static void log_event(const char *event_type, const char *path,
|
|
106
|
+
const char *marker, const char *channel, int implicit) {
|
|
107
|
+
struct timespec now;
|
|
108
|
+
struct tm utc;
|
|
109
|
+
char stamp[32];
|
|
110
|
+
char esc_path[512];
|
|
111
|
+
char esc_marker[32];
|
|
112
|
+
char esc_process[128];
|
|
113
|
+
char line[1024];
|
|
114
|
+
const char *process = program_invocation_short_name;
|
|
115
|
+
|
|
116
|
+
if (event_type == NULL) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (process == NULL || process[0] == '\0') {
|
|
120
|
+
process = "unknown";
|
|
121
|
+
}
|
|
122
|
+
if (clock_gettime(CLOCK_REALTIME, &now) != 0) {
|
|
123
|
+
now.tv_sec = 0;
|
|
124
|
+
now.tv_nsec = 0;
|
|
125
|
+
}
|
|
126
|
+
if (gmtime_r(&now.tv_sec, &utc) == NULL) {
|
|
127
|
+
memset(&utc, 0, sizeof(utc));
|
|
128
|
+
}
|
|
129
|
+
snprintf(stamp, sizeof(stamp), "%04d-%02d-%02dT%02d:%02d:%02d.%03ldZ",
|
|
130
|
+
utc.tm_year + 1900, utc.tm_mon + 1, utc.tm_mday,
|
|
131
|
+
utc.tm_hour, utc.tm_min, utc.tm_sec, now.tv_nsec / 1000000);
|
|
132
|
+
json_escape(path, esc_path, sizeof(esc_path));
|
|
133
|
+
json_escape(marker, esc_marker, sizeof(esc_marker));
|
|
134
|
+
json_escape(process, esc_process, sizeof(esc_process));
|
|
135
|
+
|
|
136
|
+
snprintf(line, sizeof(line),
|
|
137
|
+
"{\"ts\":\"%s\",\"event_type\":\"%s\",\"path\":\"%s\","
|
|
138
|
+
"\"marker\":\"%s\",\"process\":\"%s\",\"detail\":{\"channel\":\"%s\","
|
|
139
|
+
"\"implicit\":%s}}\n",
|
|
140
|
+
stamp, event_type, esc_path, esc_marker, esc_process,
|
|
141
|
+
channel == NULL ? "" : channel, implicit ? "true" : "false");
|
|
142
|
+
|
|
143
|
+
int fd = (int)syscall(SYS_openat, AT_FDCWD, LOG_PATH,
|
|
144
|
+
O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0600);
|
|
145
|
+
if (fd >= 0) {
|
|
146
|
+
size_t remaining = strlen(line);
|
|
147
|
+
while (remaining > 0) {
|
|
148
|
+
ssize_t written = syscall(SYS_write, fd, line, remaining);
|
|
149
|
+
if (written <= 0) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
remaining -= (size_t)written;
|
|
153
|
+
}
|
|
154
|
+
syscall(SYS_close, fd);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static int is_dockerenv(const char *path) {
|
|
159
|
+
return path != NULL && strcmp(path, DOCKERENV) == 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
static int is_canary_path(const char *path) {
|
|
163
|
+
return path != NULL && strncmp(path, CANARY_ROOT, strlen(CANARY_ROOT)) == 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static int is_probe_path(const char *path) {
|
|
167
|
+
if (path == NULL) {
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
if (strcmp(path, DOCKERENV) == 0 ||
|
|
171
|
+
strcmp(path, "/proc/1/cgroup") == 0 ||
|
|
172
|
+
strcmp(path, "/proc/self/cgroup") == 0 ||
|
|
173
|
+
strcmp(path, "/proc/1/mountinfo") == 0 ||
|
|
174
|
+
strcmp(path, "/proc/self/mountinfo") == 0 ||
|
|
175
|
+
strcmp(path, "/proc/bus/input/devices") == 0 ||
|
|
176
|
+
strcmp(path, "/dev/kmsg") == 0) {
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
return strncmp(path, "/sys/class/dmi/id/", 18) == 0 ||
|
|
180
|
+
strncmp(path, "/sys/hypervisor/", 16) == 0 ||
|
|
181
|
+
strncmp(path, "/proc/bus/input/", 16) == 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
static const char *redirect_path(const char *path) {
|
|
185
|
+
if (path == NULL) {
|
|
186
|
+
return path;
|
|
187
|
+
}
|
|
188
|
+
if (strcmp(path, "/proc/1/cgroup") == 0 ||
|
|
189
|
+
strcmp(path, "/proc/self/cgroup") == 0) {
|
|
190
|
+
return FAKE_CGROUP;
|
|
191
|
+
}
|
|
192
|
+
if (strcmp(path, "/proc/1/mountinfo") == 0 ||
|
|
193
|
+
strcmp(path, "/proc/self/mountinfo") == 0) {
|
|
194
|
+
return FAKE_MOUNTINFO;
|
|
195
|
+
}
|
|
196
|
+
return path;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
static int reads_content(int flags) {
|
|
200
|
+
return (flags & O_ACCMODE) != O_WRONLY;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
static void log_path_access(const char *path, int flags, int is_read) {
|
|
204
|
+
if (is_dockerenv(path)) {
|
|
205
|
+
log_event("probe_detected", path, NULL, "path", 0);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (is_probe_path(path)) {
|
|
209
|
+
log_event("probe_detected", path, NULL, "path", 0);
|
|
210
|
+
}
|
|
211
|
+
if (is_read && is_canary_path(path)) {
|
|
212
|
+
log_event("canary_read", path, NULL, "file", 0);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
static int is_command_probe(const char *path) {
|
|
217
|
+
const char *base = path;
|
|
218
|
+
if (path == NULL) {
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
const char *slash = strrchr(path, '/');
|
|
222
|
+
if (slash != NULL) {
|
|
223
|
+
base = slash + 1;
|
|
224
|
+
}
|
|
225
|
+
return strcmp(base, "df") == 0 || strcmp(base, "mount") == 0 ||
|
|
226
|
+
strcmp(base, "lsblk") == 0 ||
|
|
227
|
+
strcmp(base, "systemd-detect-virt") == 0 ||
|
|
228
|
+
strcmp(base, "dmidecode") == 0 || strcmp(base, "lscpu") == 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
typedef int (*open_fn)(const char *, int, ...);
|
|
232
|
+
typedef int (*openat_fn)(int, const char *, int, ...);
|
|
233
|
+
typedef FILE *(*fopen_fn)(const char *, const char *);
|
|
234
|
+
typedef int (*stat_fn)(const char *, struct stat *);
|
|
235
|
+
typedef int (*lstat_fn)(const char *, struct stat *);
|
|
236
|
+
typedef int (*stat64_fn)(const char *, struct stat64 *);
|
|
237
|
+
typedef int (*lstat64_fn)(const char *, struct stat64 *);
|
|
238
|
+
typedef int (*fstatat_fn)(int, const char *, struct stat *, int);
|
|
239
|
+
typedef int (*xstat_fn)(int, const char *, struct stat *);
|
|
240
|
+
typedef int (*lxstat_fn)(int, const char *, struct stat *);
|
|
241
|
+
typedef int (*xstat64_fn)(int, const char *, struct stat64 *);
|
|
242
|
+
typedef int (*lxstat64_fn)(int, const char *, struct stat64 *);
|
|
243
|
+
typedef int (*statx_fn)(int, const char *, int, unsigned int, struct statx *);
|
|
244
|
+
typedef int (*access_fn)(const char *, int);
|
|
245
|
+
typedef char *(*getenv_fn)(const char *);
|
|
246
|
+
typedef int (*execve_fn)(const char *, char *const[], char *const[]);
|
|
247
|
+
|
|
248
|
+
#define RESOLVE_OR(name, type, fallback) \
|
|
249
|
+
static type real_##name = NULL; \
|
|
250
|
+
if (real_##name == NULL) { real_##name = (type)dlsym(RTLD_NEXT, #name); } \
|
|
251
|
+
if (real_##name == NULL) { errno = ENOSYS; return fallback; }
|
|
252
|
+
|
|
253
|
+
static int do_open(const char *path, int flags, mode_t mode) {
|
|
254
|
+
log_path_access(path, flags, reads_content(flags));
|
|
255
|
+
if (is_dockerenv(path)) {
|
|
256
|
+
errno = ENOENT;
|
|
257
|
+
return -1;
|
|
258
|
+
}
|
|
259
|
+
RESOLVE_OR(open, open_fn, -1);
|
|
260
|
+
return real_open(redirect_path(path), flags, mode);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
int open(const char *path, int flags, ...) {
|
|
264
|
+
mode_t mode = 0;
|
|
265
|
+
if (flags & O_CREAT) {
|
|
266
|
+
va_list args;
|
|
267
|
+
va_start(args, flags);
|
|
268
|
+
mode = (mode_t)va_arg(args, int);
|
|
269
|
+
va_end(args);
|
|
270
|
+
}
|
|
271
|
+
return do_open(path, flags, mode);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
int open64(const char *path, int flags, ...) {
|
|
275
|
+
mode_t mode = 0;
|
|
276
|
+
if (flags & O_CREAT) {
|
|
277
|
+
va_list args;
|
|
278
|
+
va_start(args, flags);
|
|
279
|
+
mode = (mode_t)va_arg(args, int);
|
|
280
|
+
va_end(args);
|
|
281
|
+
}
|
|
282
|
+
return do_open(path, flags, mode);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
int openat(int dirfd, const char *path, int flags, ...) {
|
|
286
|
+
mode_t mode = 0;
|
|
287
|
+
if (flags & O_CREAT) {
|
|
288
|
+
va_list args;
|
|
289
|
+
va_start(args, flags);
|
|
290
|
+
mode = (mode_t)va_arg(args, int);
|
|
291
|
+
va_end(args);
|
|
292
|
+
}
|
|
293
|
+
log_path_access(path, flags, reads_content(flags));
|
|
294
|
+
if (is_dockerenv(path)) {
|
|
295
|
+
errno = ENOENT;
|
|
296
|
+
return -1;
|
|
297
|
+
}
|
|
298
|
+
RESOLVE_OR(openat, openat_fn, -1);
|
|
299
|
+
return real_openat(dirfd, redirect_path(path), flags, mode);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
int openat64(int dirfd, const char *path, int flags, ...) {
|
|
303
|
+
mode_t mode = 0;
|
|
304
|
+
if (flags & O_CREAT) {
|
|
305
|
+
va_list args;
|
|
306
|
+
va_start(args, flags);
|
|
307
|
+
mode = (mode_t)va_arg(args, int);
|
|
308
|
+
va_end(args);
|
|
309
|
+
}
|
|
310
|
+
log_path_access(path, flags, reads_content(flags));
|
|
311
|
+
if (is_dockerenv(path)) {
|
|
312
|
+
errno = ENOENT;
|
|
313
|
+
return -1;
|
|
314
|
+
}
|
|
315
|
+
RESOLVE_OR(openat, openat_fn, -1);
|
|
316
|
+
return real_openat(dirfd, redirect_path(path), flags, mode);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
FILE *fopen(const char *path, const char *mode) {
|
|
320
|
+
int is_read = mode != NULL && (mode[0] == 'r' || strchr(mode, '+') != NULL);
|
|
321
|
+
log_path_access(path, O_RDONLY, is_read);
|
|
322
|
+
if (is_dockerenv(path)) {
|
|
323
|
+
errno = ENOENT;
|
|
324
|
+
return NULL;
|
|
325
|
+
}
|
|
326
|
+
RESOLVE_OR(fopen, fopen_fn, NULL);
|
|
327
|
+
return real_fopen(redirect_path(path), mode);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
FILE *fopen64(const char *path, const char *mode) {
|
|
331
|
+
return fopen(path, mode);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
int stat(const char *path, struct stat *buf) {
|
|
335
|
+
log_path_access(path, O_RDONLY, 0);
|
|
336
|
+
if (is_dockerenv(path)) {
|
|
337
|
+
errno = ENOENT;
|
|
338
|
+
return -1;
|
|
339
|
+
}
|
|
340
|
+
RESOLVE_OR(stat, stat_fn, -1);
|
|
341
|
+
return real_stat(redirect_path(path), buf);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
int lstat(const char *path, struct stat *buf) {
|
|
345
|
+
log_path_access(path, O_RDONLY, 0);
|
|
346
|
+
if (is_dockerenv(path)) {
|
|
347
|
+
errno = ENOENT;
|
|
348
|
+
return -1;
|
|
349
|
+
}
|
|
350
|
+
RESOLVE_OR(lstat, lstat_fn, -1);
|
|
351
|
+
return real_lstat(redirect_path(path), buf);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* glibc and dash may call the large-file or versioned stat entry points
|
|
355
|
+
* instead of stat()/lstat(). Without these wrappers a plain shell
|
|
356
|
+
* ``test -e /.dockerenv`` bypasses the masquerade. */
|
|
357
|
+
int stat64(const char *path, struct stat64 *buf) {
|
|
358
|
+
log_path_access(path, O_RDONLY, 0);
|
|
359
|
+
if (is_dockerenv(path)) {
|
|
360
|
+
errno = ENOENT;
|
|
361
|
+
return -1;
|
|
362
|
+
}
|
|
363
|
+
RESOLVE_OR(stat64, stat64_fn, -1);
|
|
364
|
+
return real_stat64(redirect_path(path), buf);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
int lstat64(const char *path, struct stat64 *buf) {
|
|
368
|
+
log_path_access(path, O_RDONLY, 0);
|
|
369
|
+
if (is_dockerenv(path)) {
|
|
370
|
+
errno = ENOENT;
|
|
371
|
+
return -1;
|
|
372
|
+
}
|
|
373
|
+
RESOLVE_OR(lstat64, lstat64_fn, -1);
|
|
374
|
+
return real_lstat64(redirect_path(path), buf);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
int fstatat(int dirfd, const char *path, struct stat *buf, int flags) {
|
|
378
|
+
log_path_access(path, O_RDONLY, 0);
|
|
379
|
+
if (is_dockerenv(path)) {
|
|
380
|
+
errno = ENOENT;
|
|
381
|
+
return -1;
|
|
382
|
+
}
|
|
383
|
+
RESOLVE_OR(fstatat, fstatat_fn, -1);
|
|
384
|
+
return real_fstatat(dirfd, redirect_path(path), buf, flags);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
int newfstatat(int dirfd, const char *path, struct stat *buf, int flags) {
|
|
388
|
+
return fstatat(dirfd, path, buf, flags);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
int statx(int dirfd, const char *path, int flags, unsigned int mask, struct statx *buf) {
|
|
392
|
+
log_path_access(path, O_RDONLY, 0);
|
|
393
|
+
if (is_dockerenv(path)) {
|
|
394
|
+
errno = ENOENT;
|
|
395
|
+
return -1;
|
|
396
|
+
}
|
|
397
|
+
RESOLVE_OR(statx, statx_fn, -1);
|
|
398
|
+
return real_statx(dirfd, redirect_path(path), flags, mask, buf);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
int __xstat(int version, const char *path, struct stat *buf) {
|
|
402
|
+
log_path_access(path, O_RDONLY, 0);
|
|
403
|
+
if (is_dockerenv(path)) {
|
|
404
|
+
errno = ENOENT;
|
|
405
|
+
return -1;
|
|
406
|
+
}
|
|
407
|
+
RESOLVE_OR(__xstat, xstat_fn, -1);
|
|
408
|
+
return real___xstat(version, redirect_path(path), buf);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
int __lxstat(int version, const char *path, struct stat *buf) {
|
|
412
|
+
log_path_access(path, O_RDONLY, 0);
|
|
413
|
+
if (is_dockerenv(path)) {
|
|
414
|
+
errno = ENOENT;
|
|
415
|
+
return -1;
|
|
416
|
+
}
|
|
417
|
+
RESOLVE_OR(__lxstat, lxstat_fn, -1);
|
|
418
|
+
return real___lxstat(version, redirect_path(path), buf);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
int __xstat64(int version, const char *path, struct stat64 *buf) {
|
|
422
|
+
log_path_access(path, O_RDONLY, 0);
|
|
423
|
+
if (is_dockerenv(path)) {
|
|
424
|
+
errno = ENOENT;
|
|
425
|
+
return -1;
|
|
426
|
+
}
|
|
427
|
+
RESOLVE_OR(__xstat64, xstat64_fn, -1);
|
|
428
|
+
return real___xstat64(version, redirect_path(path), buf);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
int __lxstat64(int version, const char *path, struct stat64 *buf) {
|
|
432
|
+
log_path_access(path, O_RDONLY, 0);
|
|
433
|
+
if (is_dockerenv(path)) {
|
|
434
|
+
errno = ENOENT;
|
|
435
|
+
return -1;
|
|
436
|
+
}
|
|
437
|
+
RESOLVE_OR(__lxstat64, lxstat64_fn, -1);
|
|
438
|
+
return real___lxstat64(version, redirect_path(path), buf);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
int access(const char *path, int mode) {
|
|
442
|
+
log_path_access(path, O_RDONLY, 0);
|
|
443
|
+
if (is_dockerenv(path)) {
|
|
444
|
+
errno = ENOENT;
|
|
445
|
+
return -1;
|
|
446
|
+
}
|
|
447
|
+
RESOLVE_OR(access, access_fn, -1);
|
|
448
|
+
return real_access(redirect_path(path), mode);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
int faccessat(int dirfd, const char *path, int mode, int flags) {
|
|
452
|
+
(void)dirfd;
|
|
453
|
+
(void)flags;
|
|
454
|
+
return access(path, mode);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
char *getenv(const char *name) {
|
|
458
|
+
RESOLVE_OR(getenv, getenv_fn, NULL);
|
|
459
|
+
char *value = real_getenv(name);
|
|
460
|
+
if (value != NULL && strstr(value, "SKS") != NULL) {
|
|
461
|
+
char marker[14];
|
|
462
|
+
marker_from(value, marker);
|
|
463
|
+
if (marker[0] != '\0') {
|
|
464
|
+
char path[256];
|
|
465
|
+
snprintf(path, sizeof(path), "env:%s", name == NULL ? "" : name);
|
|
466
|
+
log_event("canary_read", path, marker, "env", 1);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return value;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
char *secure_getenv(const char *name) {
|
|
473
|
+
return getenv(name);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
int execve(const char *path, char *const argv[], char *const envp[]) {
|
|
477
|
+
(void)argv;
|
|
478
|
+
(void)envp;
|
|
479
|
+
if (is_command_probe(path)) {
|
|
480
|
+
log_event("probe_detected", path, NULL, "command", 0);
|
|
481
|
+
}
|
|
482
|
+
RESOLVE_OR(execve, execve_fn, -1);
|
|
483
|
+
return real_execve(path, argv, envp);
|
|
484
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Skill entrypoint used when the package does not supply an explicit command.
|
|
3
|
+
# The runner uploads this file into the container's writable /tmp tmpfs.
|
|
4
|
+
set -u
|
|
5
|
+
umask 077
|
|
6
|
+
export HOME="${HOME:-/home/sandbox}"
|
|
7
|
+
cd /skill || exit 1
|
|
8
|
+
|
|
9
|
+
# A declared entrypoint is authoritative. Never fall through to a different
|
|
10
|
+
# script (or exit 0) when the manifest asked for something that is missing.
|
|
11
|
+
if [ -n "${SKILLSENTRY_ENTRYPOINT:-}" ]; then
|
|
12
|
+
if [ ! -f "$SKILLSENTRY_ENTRYPOINT" ]; then
|
|
13
|
+
echo "skillsentry: declared entrypoint not found: $SKILLSENTRY_ENTRYPOINT" >&2
|
|
14
|
+
exit 127
|
|
15
|
+
fi
|
|
16
|
+
# Args are deliberately word-split here: the manifest is untrusted and the
|
|
17
|
+
# sandbox is the execution boundary. The engine prefers an explicit argv
|
|
18
|
+
# and only uses this path for compatibility.
|
|
19
|
+
# shellcheck disable=SC2086
|
|
20
|
+
exec "$SKILLSENTRY_ENTRYPOINT" ${SKILLSENTRY_ENTRYPOINT_ARGS:-}
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if [ "${SKILLSENTRY_DECLARED_ENTRYPOINT:-0}" = "1" ]; then
|
|
24
|
+
echo "skillsentry: manifest declared an entrypoint but no command was supplied" >&2
|
|
25
|
+
exit 127
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if [ -x ./scripts/run.sh ]; then
|
|
29
|
+
exec ./scripts/run.sh "$@"
|
|
30
|
+
fi
|
|
31
|
+
if [ -x ./run.sh ]; then
|
|
32
|
+
exec ./run.sh "$@"
|
|
33
|
+
fi
|
|
34
|
+
if [ -f ./scripts/run.py ]; then
|
|
35
|
+
exec python ./scripts/run.py "$@"
|
|
36
|
+
fi
|
|
37
|
+
if [ -f ./main.py ]; then
|
|
38
|
+
exec python ./main.py "$@"
|
|
39
|
+
fi
|
|
40
|
+
# A package with only a SKILL.md is a valid, inert skill.
|
|
41
|
+
exit 0
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Inert DNS + HTTP sinkhole sidecar (stdlib only, runs inside Docker)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import contextlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import socket
|
|
10
|
+
import struct
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
15
|
+
|
|
16
|
+
ANSWER_IP = os.environ.get("SKILLSENTRY_SIDECAR_IP", "172.30.77.2")
|
|
17
|
+
DNS_PORT = int(os.environ.get("SKILLSENTRY_DNS_PORT", "53"))
|
|
18
|
+
HTTP_PORT = int(os.environ.get("SKILLSENTRY_HTTP_PORT", "80"))
|
|
19
|
+
MAX_BODY = 2_000_000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _stamp() -> str:
|
|
23
|
+
now = datetime.now(UTC)
|
|
24
|
+
return now.strftime("%Y-%m-%dT%H:%M:%S.") + f"{now.microsecond // 1000:03d}Z"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _emit(payload: dict) -> None:
|
|
28
|
+
sys.stdout.write(json.dumps(payload, separators=(",", ":")) + "\n")
|
|
29
|
+
sys.stdout.flush()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _question_end(data: bytes) -> int | None:
|
|
33
|
+
if len(data) < 12:
|
|
34
|
+
return None
|
|
35
|
+
offset = 12
|
|
36
|
+
while offset < len(data):
|
|
37
|
+
length = data[offset]
|
|
38
|
+
offset += 1
|
|
39
|
+
if length == 0:
|
|
40
|
+
return offset + 4 if offset + 4 <= len(data) else None
|
|
41
|
+
if length & 0xC0 or offset + length > len(data):
|
|
42
|
+
return None
|
|
43
|
+
offset += length
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _qname(data: bytes) -> str:
|
|
48
|
+
labels: list[str] = []
|
|
49
|
+
offset = 12
|
|
50
|
+
while offset < len(data):
|
|
51
|
+
length = data[offset]
|
|
52
|
+
offset += 1
|
|
53
|
+
if length == 0:
|
|
54
|
+
break
|
|
55
|
+
if length & 0xC0 or offset + length > len(data):
|
|
56
|
+
return ""
|
|
57
|
+
labels.append(data[offset : offset + length].decode("ascii", "replace"))
|
|
58
|
+
offset += length
|
|
59
|
+
return ".".join(labels) or "."
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _response(query: bytes) -> bytes:
|
|
63
|
+
if len(query) < 12:
|
|
64
|
+
return b""
|
|
65
|
+
end = _question_end(query)
|
|
66
|
+
if end is None:
|
|
67
|
+
return b""
|
|
68
|
+
qtype = struct.unpack("!H", query[end - 4 : end - 2])[0]
|
|
69
|
+
answer = b""
|
|
70
|
+
ancount = 0
|
|
71
|
+
if qtype == 1:
|
|
72
|
+
try:
|
|
73
|
+
packed = socket.inet_aton(ANSWER_IP)
|
|
74
|
+
except OSError:
|
|
75
|
+
packed = b"\xac\x1eM\x02"
|
|
76
|
+
answer = b"\xc0\x0c" + struct.pack("!HHIH", 1, 1, 60, 4) + packed
|
|
77
|
+
ancount = 1
|
|
78
|
+
flags = 0x8000 | 0x0100 | 0x0080
|
|
79
|
+
return query[:2] + struct.pack("!HHHHH", flags, 1, ancount, 0, 0) + query[12:end] + answer
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def dns_loop(sock: socket.socket | None = None) -> None:
|
|
83
|
+
if sock is None:
|
|
84
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
85
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
86
|
+
sock.bind(("0.0.0.0", DNS_PORT))
|
|
87
|
+
while True:
|
|
88
|
+
try:
|
|
89
|
+
data, peer = sock.recvfrom(4096)
|
|
90
|
+
except OSError:
|
|
91
|
+
continue
|
|
92
|
+
qname = _qname(data)
|
|
93
|
+
if not qname:
|
|
94
|
+
continue
|
|
95
|
+
end = _question_end(data)
|
|
96
|
+
qtype = struct.unpack("!H", data[end - 4 : end - 2])[0] if end else 0
|
|
97
|
+
_emit(
|
|
98
|
+
{
|
|
99
|
+
"ts": _stamp(),
|
|
100
|
+
"protocol": "dns",
|
|
101
|
+
"query_or_url": qname[:4096],
|
|
102
|
+
"body_b64": base64.b64encode(data[:MAX_BODY]).decode("ascii"),
|
|
103
|
+
"headers": {},
|
|
104
|
+
"client": f"{peer[0]}:{peer[1]}",
|
|
105
|
+
"responder_action": "fake-A" if qtype == 1 else "NOERROR",
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
with contextlib.suppress(OSError):
|
|
109
|
+
sock.sendto(_response(data), peer)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Handler(BaseHTTPRequestHandler):
|
|
113
|
+
def log_message(self, fmt, *args):
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
def _handle(self) -> None:
|
|
117
|
+
raw_length = self.headers.get("Content-Length", "0")
|
|
118
|
+
try:
|
|
119
|
+
length = max(0, min(int(raw_length), MAX_BODY))
|
|
120
|
+
except ValueError:
|
|
121
|
+
length = 0
|
|
122
|
+
body = self.rfile.read(length) if length else b""
|
|
123
|
+
host = self.headers.get("Host", "")
|
|
124
|
+
url = f"http://{host}{self.path}" if host else self.path
|
|
125
|
+
_emit(
|
|
126
|
+
{
|
|
127
|
+
"ts": _stamp(),
|
|
128
|
+
"protocol": "http",
|
|
129
|
+
"query_or_url": f"{self.command} {url}"[:4096],
|
|
130
|
+
"body_b64": base64.b64encode(body).decode("ascii") if body else None,
|
|
131
|
+
"headers": {str(k)[:100]: str(v)[:1000] for k, v in self.headers.items()},
|
|
132
|
+
"client": f"{self.client_address[0]}:{self.client_address[1]}",
|
|
133
|
+
"responder_action": "200+secondary",
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
encoded = b'{"status":"ok","data":"inert"}'
|
|
137
|
+
self.send_response(200)
|
|
138
|
+
self.send_header("Content-Type", "application/json")
|
|
139
|
+
self.send_header("Content-Length", str(len(encoded)))
|
|
140
|
+
self.send_header("Connection", "close")
|
|
141
|
+
self.end_headers()
|
|
142
|
+
self.wfile.write(encoded)
|
|
143
|
+
|
|
144
|
+
do_GET = _handle
|
|
145
|
+
do_POST = _handle
|
|
146
|
+
do_PUT = _handle
|
|
147
|
+
do_PATCH = _handle
|
|
148
|
+
do_DELETE = _handle
|
|
149
|
+
do_HEAD = _handle
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def main() -> None:
|
|
153
|
+
dns_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
154
|
+
dns_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
155
|
+
dns_sock.bind(("0.0.0.0", DNS_PORT))
|
|
156
|
+
threading.Thread(target=dns_loop, args=(dns_sock,), daemon=True).start()
|
|
157
|
+
server = ThreadingHTTPServer(("0.0.0.0", HTTP_PORT), Handler)
|
|
158
|
+
server.daemon_threads = True
|
|
159
|
+
_emit(
|
|
160
|
+
{
|
|
161
|
+
"event": "ready",
|
|
162
|
+
"dns_port": DNS_PORT,
|
|
163
|
+
"http_port": HTTP_PORT,
|
|
164
|
+
"ip": ANSWER_IP,
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
server.serve_forever()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
main()
|