opencode-skills-collection 3.1.5 ā 3.1.7
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/bundled-skills/.antigravity-install-manifest.json +4 -1
- package/bundled-skills/007/scripts/scanners/dependency_scanner.py +2 -2
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/docx-official/ooxml/scripts/pack.py +12 -0
- package/bundled-skills/docx-official/ooxml/scripts/unpack.py +28 -7
- package/bundled-skills/docx-official/ooxml/scripts/validation/base.py +18 -13
- package/bundled-skills/docx-official/ooxml/scripts/validation/docx.py +6 -6
- package/bundled-skills/docx-official/ooxml/scripts/validation/pptx.py +6 -6
- package/bundled-skills/emil-design-eng/SKILL.md +702 -0
- package/bundled-skills/hugging-face-model-trainer/scripts/convert_to_gguf.py +3 -2
- package/bundled-skills/infinity/SKILL.md +173 -0
- package/bundled-skills/loki-mode/autonomy/run.sh +17 -8
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package-lock.json +17 -13
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package.json +3 -3
- package/bundled-skills/macos-spm-app-packaging/assets/templates/setup_dev_signing.sh +11 -8
- package/bundled-skills/macos-spm-app-packaging/assets/templates/sign-and-notarize.sh +10 -5
- package/bundled-skills/pptx-official/ooxml/scripts/pack.py +12 -0
- package/bundled-skills/pptx-official/ooxml/scripts/unpack.py +28 -7
- package/bundled-skills/pptx-official/ooxml/scripts/validation/base.py +18 -13
- package/bundled-skills/pptx-official/ooxml/scripts/validation/docx.py +6 -6
- package/bundled-skills/pptx-official/ooxml/scripts/validation/pptx.py +6 -6
- package/bundled-skills/review-animations/SKILL.md +139 -0
- package/bundled-skills/review-animations/STANDARDS.md +188 -0
- package/bundled-skills/skill-creator/scripts/package_skill.py +11 -1
- package/package.json +2 -2
- package/skills_index.json +66 -0
|
@@ -132,6 +132,7 @@ ADAPTER_MODEL = require_hf_id(os.environ.get("ADAPTER_MODEL", "evalstate/qwen-ca
|
|
|
132
132
|
BASE_MODEL = require_hf_id(os.environ.get("BASE_MODEL", "Qwen/Qwen2.5-0.5B"), "BASE_MODEL")
|
|
133
133
|
OUTPUT_REPO = require_hf_id(os.environ.get("OUTPUT_REPO", "evalstate/qwen-capybara-medium-gguf"), "OUTPUT_REPO")
|
|
134
134
|
username = require_hf_id(os.environ.get("HF_USERNAME", ADAPTER_MODEL.split('/')[0]), "HF_USERNAME")
|
|
135
|
+
TRUST_REMOTE_CODE = os.environ.get("TRUST_REMOTE_CODE", "").strip().lower() in {"1", "true", "yes"}
|
|
135
136
|
|
|
136
137
|
print(f"\nš¦ Configuration:")
|
|
137
138
|
print(f" Base model: {BASE_MODEL}")
|
|
@@ -147,7 +148,7 @@ try:
|
|
|
147
148
|
BASE_MODEL,
|
|
148
149
|
dtype=torch.float16,
|
|
149
150
|
device_map="auto",
|
|
150
|
-
trust_remote_code=
|
|
151
|
+
trust_remote_code=TRUST_REMOTE_CODE,
|
|
151
152
|
)
|
|
152
153
|
print(" ā
Base model loaded")
|
|
153
154
|
except Exception as e:
|
|
@@ -169,7 +170,7 @@ except Exception as e:
|
|
|
169
170
|
|
|
170
171
|
try:
|
|
171
172
|
# Load tokenizer
|
|
172
|
-
tokenizer = AutoTokenizer.from_pretrained(ADAPTER_MODEL, trust_remote_code=
|
|
173
|
+
tokenizer = AutoTokenizer.from_pretrained(ADAPTER_MODEL, trust_remote_code=TRUST_REMOTE_CODE)
|
|
173
174
|
print(" ā
Tokenizer loaded")
|
|
174
175
|
except Exception as e:
|
|
175
176
|
print(f" ā Failed to load tokenizer: {e}")
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: infinity
|
|
3
|
+
description: "Enforces a strict input boundary protocol (detect, classify, filter, verify) to ensure untrusted data never reaches business logic raw."
|
|
4
|
+
risk: safe
|
|
5
|
+
source: community
|
|
6
|
+
date_added: "2026-06-23"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# infinity ā Input Boundary & Validation Protocol
|
|
10
|
+
|
|
11
|
+
## Core Philosophy
|
|
12
|
+
|
|
13
|
+
> Nothing untrusted ever reaches the core ā it is stopped before contact. No external data touches the codebase raw. Every boundary where data enters the system must have a filter.
|
|
14
|
+
|
|
15
|
+
The #1 source of silent bugs, crashes, and vulnerabilities is external data that arrives in an unexpected shape and gets used directly without checking. This skill enforces a filter layer at every entry point, every time.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## When to Use This Skill
|
|
20
|
+
|
|
21
|
+
- Use when you need to handle an API response
|
|
22
|
+
- Use when reading user input or adding a form handler
|
|
23
|
+
- Use when working with environment variables or CLI arguments
|
|
24
|
+
- Use when parsing webhooks or reading from the filesystem
|
|
25
|
+
- Use when any code calls `.body`, `.params`, `.query`, `.env`, `fs.read`, or a third-party SDK response
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## The Four Phases
|
|
30
|
+
|
|
31
|
+
### PHASE 1 ā Boundary Detection
|
|
32
|
+
|
|
33
|
+
Before writing or modifying any code that involves external data, the AI must identify and list every entry point in scope:
|
|
34
|
+
|
|
35
|
+
- HTTP request bodies, headers, query params
|
|
36
|
+
- User form inputs and UI-submitted data
|
|
37
|
+
- Environment variables and config files
|
|
38
|
+
- Third-party API responses
|
|
39
|
+
- Webhook payloads
|
|
40
|
+
- File reads from disk
|
|
41
|
+
- CLI arguments
|
|
42
|
+
- Database query results from external sources
|
|
43
|
+
- WebSocket messages
|
|
44
|
+
|
|
45
|
+
> **The AI must not write any data-handling logic until every entry point in scope is listed.**
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### PHASE 2 ā Classify Each Input
|
|
50
|
+
|
|
51
|
+
For every entry point identified, the AI classifies it into one of three trust levels:
|
|
52
|
+
|
|
53
|
+
| Level | Definition | Examples |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `TRUSTED` | Internal constants, hardcoded values, your own compile-time config | Enum values, hardcoded defaults, internal constants |
|
|
56
|
+
| `SEMI-TRUSTED` | Your own internal services, internal APIs, controlled infrastructure | Internal microservice responses, your own database reads |
|
|
57
|
+
| `UNTRUSTED` | Anything from users, the internet, third parties, or the filesystem | User input, external API responses, uploaded files, env vars, CLI args |
|
|
58
|
+
|
|
59
|
+
> **Rule:** `TRUSTED` inputs may be used directly. `SEMI-TRUSTED` and `UNTRUSTED` inputs must pass through a filter layer before any use.
|
|
60
|
+
|
|
61
|
+
The AI outputs this classification before writing any handling code:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
INFINITY ā BOUNDARY MAP
|
|
65
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
66
|
+
Entry Point | Trust Level | Filter Required
|
|
67
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
68
|
+
req.body.email | UNTRUSTED | ā format + sanitize
|
|
69
|
+
process.env.API_KEY | UNTRUSTED | ā presence + non-empty
|
|
70
|
+
internalService.getData()| SEMI-TRUSTED | ā schema validate
|
|
71
|
+
PAGINATION_LIMIT = 20 | TRUSTED | ā none needed
|
|
72
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### PHASE 3 ā Mandatory Filter Layer
|
|
78
|
+
|
|
79
|
+
Every `UNTRUSTED` and `SEMI-TRUSTED` input must pass through validation before it reaches any business logic, storage, or rendering. The AI must apply the right filter type for the right context:
|
|
80
|
+
|
|
81
|
+
**Type Checking**
|
|
82
|
+
- Verify the input is the expected type before using it
|
|
83
|
+
- Never assume a string is a string, a number is a number, or an array is an array
|
|
84
|
+
|
|
85
|
+
**Schema Validation**
|
|
86
|
+
- For objects and API responses, validate shape before accessing nested fields
|
|
87
|
+
- If a required field is missing, reject ā do not use a fallback that hides the problem
|
|
88
|
+
|
|
89
|
+
**Sanitization**
|
|
90
|
+
- Strip or escape content before rendering to UI (prevent XSS)
|
|
91
|
+
- Normalize strings before storage (trim whitespace, consistent casing where appropriate)
|
|
92
|
+
|
|
93
|
+
**Presence & Format Checks**
|
|
94
|
+
- Env vars: must exist and be non-empty before use
|
|
95
|
+
- IDs and tokens: must match expected format before use
|
|
96
|
+
|
|
97
|
+
**Rejection Rule**
|
|
98
|
+
- On invalid input: reject explicitly and return a clear error
|
|
99
|
+
- Never silently use bad data with a fallback
|
|
100
|
+
- Never let bad data pass through to fix itself "downstream"
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
// WRONG ā using raw input directly
|
|
104
|
+
const user = await db.find(req.params.id);
|
|
105
|
+
|
|
106
|
+
// RIGHT ā validate before use
|
|
107
|
+
const id = req.params.id;
|
|
108
|
+
if (!id || typeof id !== 'string' || !isValidUUID(id)) {
|
|
109
|
+
return res.status(400).json({ error: 'Invalid ID format' });
|
|
110
|
+
}
|
|
111
|
+
const user = await db.find(id);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### PHASE 4 ā Self-Check Before Done
|
|
117
|
+
|
|
118
|
+
Before the AI declares any data-handling code complete, it traces each entry point and confirms:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
INFINITY ā VERIFICATION
|
|
122
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
123
|
+
Entry Point | Filter Exists | Filter Type
|
|
124
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
125
|
+
req.body.email | ā YES | format + sanitize
|
|
126
|
+
process.env.API_KEY | ā YES | presence check
|
|
127
|
+
internalService.getData()| ā YES | schema validation
|
|
128
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
129
|
+
Unfiltered inputs reaching logic: NONE ā
|
|
130
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
If any `UNTRUSTED` or `SEMI-TRUSTED` input reaches logic, storage, or rendering without a filter ā the AI flags it. It does not silently pass.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Hard Rules (Never Violated)
|
|
138
|
+
|
|
139
|
+
- **No raw external data in business logic.** Ever.
|
|
140
|
+
- **No silent fallbacks on bad input.** Reject explicitly.
|
|
141
|
+
- **No assuming shape.** Even if the API "always" returns a string ā validate it.
|
|
142
|
+
- **No skipping env var checks.** Missing env vars must fail loudly at startup, not silently at runtime.
|
|
143
|
+
- **No partial filtering.** If you validate presence but not format, it is not filtered.
|
|
144
|
+
- **No filtering in the wrong place.** Filters go at the entry point ā not somewhere downstream after the data has already been used once.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## What This Skill Prevents
|
|
149
|
+
|
|
150
|
+
- SQL injection via unvalidated query params
|
|
151
|
+
- Crashes from unexpected API response shapes
|
|
152
|
+
- XSS from unescaped user content rendered to UI
|
|
153
|
+
- Silent failures from missing env variables discovered at runtime
|
|
154
|
+
- Type errors from assuming external data matches expected shape
|
|
155
|
+
- Security vulnerabilities from untrusted data reaching sensitive operations
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Quick Reference
|
|
160
|
+
|
|
161
|
+
| Phase | Action | Writes Code? |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| 1 ā Detect | List all entry points in scope | ā No |
|
|
164
|
+
| 2 ā Classify | Assign trust level to each input | ā No |
|
|
165
|
+
| 3 ā Filter | Write filter layer for all UNTRUSTED + SEMI-TRUSTED | ā
Yes |
|
|
166
|
+
| 4 ā Verify | Trace each input, confirm filter exists | ā No |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Limitations
|
|
171
|
+
|
|
172
|
+
- Does not apply to purely internal logic with no external data involvement.
|
|
173
|
+
- May add verbosity to trivial scripts where strict validation is not required.
|
|
@@ -711,21 +711,30 @@ generate_dashboard() {
|
|
|
711
711
|
if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
|
|
712
712
|
return Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm';
|
|
713
713
|
}
|
|
714
|
+
function escapeHtml(value) {
|
|
715
|
+
return String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
|
716
|
+
'&': '&',
|
|
717
|
+
'<': '<',
|
|
718
|
+
'>': '>',
|
|
719
|
+
'"': '"',
|
|
720
|
+
"'": '''
|
|
721
|
+
})[char]);
|
|
722
|
+
}
|
|
714
723
|
function renderAgent(agent) {
|
|
715
724
|
const modelClass = getModelClass(agent.model);
|
|
716
|
-
const modelName = agent.model || 'Sonnet 4.5';
|
|
717
|
-
const agentType = agent.agent_type || 'general-purpose';
|
|
725
|
+
const modelName = escapeHtml(agent.model || 'Sonnet 4.5');
|
|
726
|
+
const agentType = escapeHtml(agent.agent_type || 'general-purpose');
|
|
718
727
|
const status = agent.status === 'completed' ? 'completed' : 'active';
|
|
719
|
-
const currentTask = agent.current_task || (agent.tasks_completed && agent.tasks_completed.length > 0
|
|
728
|
+
const currentTask = escapeHtml(agent.current_task || (agent.tasks_completed && agent.tasks_completed.length > 0
|
|
720
729
|
? 'Completed: ' + agent.tasks_completed.join(', ')
|
|
721
|
-
: 'Initializing...');
|
|
730
|
+
: 'Initializing...'));
|
|
722
731
|
const duration = formatDuration(agent.spawned_at);
|
|
723
732
|
const tasksCount = agent.tasks_completed ? agent.tasks_completed.length : 0;
|
|
724
733
|
|
|
725
734
|
return `
|
|
726
735
|
<div class="agent-card">
|
|
727
736
|
<div class="agent-header">
|
|
728
|
-
<div class="agent-id">${agent.agent_id || 'Unknown'}</div>
|
|
737
|
+
<div class="agent-id">${escapeHtml(agent.agent_id || 'Unknown')}</div>
|
|
729
738
|
<div class="model-badge ${modelClass}">${modelName}</div>
|
|
730
739
|
</div>
|
|
731
740
|
<div class="agent-type">${agentType}</div>
|
|
@@ -740,9 +749,9 @@ generate_dashboard() {
|
|
|
740
749
|
}
|
|
741
750
|
function renderTask(task) {
|
|
742
751
|
const payload = task.payload || {};
|
|
743
|
-
const title = payload.description || payload.action || task.type || 'Task';
|
|
744
|
-
const error = task.lastError ? `<div class="error">${task.lastError}</div>` : '';
|
|
745
|
-
return `<div class="task"><div class="id">${task.id}</div><span class="type">${task.type || 'general'}</span><div class="title">${title}</div>${error}</div>`;
|
|
752
|
+
const title = escapeHtml(payload.description || payload.action || task.type || 'Task');
|
|
753
|
+
const error = task.lastError ? `<div class="error">${escapeHtml(task.lastError)}</div>` : '';
|
|
754
|
+
return `<div class="task"><div class="id">${escapeHtml(task.id)}</div><span class="type">${escapeHtml(task.type || 'general')}</span><div class="title">${title}</div>${error}</div>`;
|
|
746
755
|
}
|
|
747
756
|
async function loadData() {
|
|
748
757
|
const [pending, progress, completed, failed, agents] = await Promise.all([
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"name": "todo-app-backend",
|
|
9
9
|
"version": "1.0.0",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"better-sqlite3": "^12.
|
|
12
|
-
"cors": "^2.8.
|
|
11
|
+
"better-sqlite3": "^12.10.0",
|
|
12
|
+
"cors": "^2.8.6",
|
|
13
13
|
"express": "^4.18.2",
|
|
14
|
-
"express-rate-limit": "^8.5.
|
|
14
|
+
"express-rate-limit": "^8.5.2",
|
|
15
15
|
"ip-address": "^10.2.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
@@ -303,9 +303,9 @@
|
|
|
303
303
|
"license": "MIT"
|
|
304
304
|
},
|
|
305
305
|
"node_modules/better-sqlite3": {
|
|
306
|
-
"version": "12.
|
|
307
|
-
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.
|
|
308
|
-
"integrity": "sha512-
|
|
306
|
+
"version": "12.10.0",
|
|
307
|
+
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz",
|
|
308
|
+
"integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==",
|
|
309
309
|
"hasInstallScript": true,
|
|
310
310
|
"license": "MIT",
|
|
311
311
|
"dependencies": {
|
|
@@ -313,7 +313,7 @@
|
|
|
313
313
|
"prebuild-install": "^7.1.1"
|
|
314
314
|
},
|
|
315
315
|
"engines": {
|
|
316
|
-
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
|
|
316
|
+
"node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
|
|
317
317
|
}
|
|
318
318
|
},
|
|
319
319
|
"node_modules/bindings": {
|
|
@@ -459,9 +459,9 @@
|
|
|
459
459
|
"license": "MIT"
|
|
460
460
|
},
|
|
461
461
|
"node_modules/cors": {
|
|
462
|
-
"version": "2.8.
|
|
463
|
-
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.
|
|
464
|
-
"integrity": "sha512-
|
|
462
|
+
"version": "2.8.6",
|
|
463
|
+
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
|
464
|
+
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
|
|
465
465
|
"license": "MIT",
|
|
466
466
|
"dependencies": {
|
|
467
467
|
"object-assign": "^4",
|
|
@@ -469,6 +469,10 @@
|
|
|
469
469
|
},
|
|
470
470
|
"engines": {
|
|
471
471
|
"node": ">= 0.10"
|
|
472
|
+
},
|
|
473
|
+
"funding": {
|
|
474
|
+
"type": "opencollective",
|
|
475
|
+
"url": "https://opencollective.com/express"
|
|
472
476
|
}
|
|
473
477
|
},
|
|
474
478
|
"node_modules/create-require": {
|
|
@@ -688,9 +692,9 @@
|
|
|
688
692
|
}
|
|
689
693
|
},
|
|
690
694
|
"node_modules/express-rate-limit": {
|
|
691
|
-
"version": "8.5.
|
|
692
|
-
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.
|
|
693
|
-
"integrity": "sha512-
|
|
695
|
+
"version": "8.5.2",
|
|
696
|
+
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
|
|
697
|
+
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
|
|
694
698
|
"license": "MIT",
|
|
695
699
|
"dependencies": {
|
|
696
700
|
"ip-address": "^10.2.0"
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"dev": "ts-node src/index.ts"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"better-sqlite3": "^12.
|
|
13
|
-
"cors": "^2.8.
|
|
12
|
+
"better-sqlite3": "^12.10.0",
|
|
13
|
+
"cors": "^2.8.6",
|
|
14
14
|
"express": "^4.18.2",
|
|
15
|
-
"express-rate-limit": "^8.5.
|
|
15
|
+
"express-rate-limit": "^8.5.2",
|
|
16
16
|
"ip-address": "^10.2.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
@@ -14,8 +14,13 @@ fi
|
|
|
14
14
|
|
|
15
15
|
echo "Creating self-signed certificate '$CERT_NAME'..."
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
TEMP_DIR=$(mktemp -d)
|
|
18
|
+
chmod 700 "$TEMP_DIR"
|
|
19
|
+
TEMP_CONFIG="$TEMP_DIR/dev.cnf"
|
|
20
|
+
KEY_PATH="$TEMP_DIR/dev.key"
|
|
21
|
+
CRT_PATH="$TEMP_DIR/dev.crt"
|
|
22
|
+
P12_PATH="$TEMP_DIR/dev.p12"
|
|
23
|
+
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
19
24
|
|
|
20
25
|
cat > "$TEMP_CONFIG" <<EOFCONF
|
|
21
26
|
[ req ]
|
|
@@ -34,18 +39,16 @@ extendedKeyUsage = codeSigning
|
|
|
34
39
|
EOFCONF
|
|
35
40
|
|
|
36
41
|
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \
|
|
37
|
-
-nodes -keyout
|
|
42
|
+
-nodes -keyout "$KEY_PATH" -out "$CRT_PATH" \
|
|
38
43
|
-config "$TEMP_CONFIG" 2>/dev/null
|
|
39
44
|
|
|
40
|
-
openssl pkcs12 -export -out
|
|
41
|
-
-inkey
|
|
45
|
+
openssl pkcs12 -export -out "$P12_PATH" \
|
|
46
|
+
-inkey "$KEY_PATH" -in "$CRT_PATH" \
|
|
42
47
|
-passout pass: 2>/dev/null
|
|
43
48
|
|
|
44
|
-
security import
|
|
49
|
+
security import "$P12_PATH" -k ~/Library/Keychains/login.keychain-db \
|
|
45
50
|
-T /usr/bin/codesign -T /usr/bin/security
|
|
46
51
|
|
|
47
|
-
rm -f /tmp/dev.{key,crt,p12}
|
|
48
|
-
|
|
49
52
|
echo ""
|
|
50
53
|
echo "Trust this certificate for code signing in Keychain Access."
|
|
51
54
|
echo "Then export in your shell profile:"
|
|
@@ -13,8 +13,13 @@ if [[ -z "${APP_STORE_CONNECT_API_KEY_P8:-}" || -z "${APP_STORE_CONNECT_KEY_ID:-
|
|
|
13
13
|
exit 1
|
|
14
14
|
fi
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
TEMP_DIR=$(mktemp -d)
|
|
17
|
+
chmod 700 "$TEMP_DIR"
|
|
18
|
+
KEY_PATH="$TEMP_DIR/app-store-connect-key.p8"
|
|
19
|
+
NOTARY_ZIP="$TEMP_DIR/${APP_NAME}Notarize.zip"
|
|
20
|
+
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
21
|
+
|
|
22
|
+
echo "$APP_STORE_CONNECT_API_KEY_P8" | sed 's/\\n/\n/g' > "$KEY_PATH"
|
|
18
23
|
|
|
19
24
|
ARCHES_VALUE=${ARCHES:-"arm64 x86_64"}
|
|
20
25
|
ARCH_LIST=( ${ARCHES_VALUE} )
|
|
@@ -31,10 +36,10 @@ codesign --force --timestamp --options runtime --sign "$APP_IDENTITY" \
|
|
|
31
36
|
"$APP_BUNDLE"
|
|
32
37
|
|
|
33
38
|
DITTO_BIN=${DITTO_BIN:-/usr/bin/ditto}
|
|
34
|
-
"$DITTO_BIN" --norsrc -c -k --keepParent "$APP_BUNDLE" "
|
|
39
|
+
"$DITTO_BIN" --norsrc -c -k --keepParent "$APP_BUNDLE" "$NOTARY_ZIP"
|
|
35
40
|
|
|
36
|
-
xcrun notarytool submit "
|
|
37
|
-
--key
|
|
41
|
+
xcrun notarytool submit "$NOTARY_ZIP" \
|
|
42
|
+
--key "$KEY_PATH" \
|
|
38
43
|
--key-id "$APP_STORE_CONNECT_KEY_ID" \
|
|
39
44
|
--issuer "$APP_STORE_CONNECT_ISSUER_ID" \
|
|
40
45
|
--wait
|
|
@@ -16,6 +16,17 @@ import zipfile
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def validate_input_tree(input_dir: Path):
|
|
20
|
+
root = input_dir.resolve(strict=True)
|
|
21
|
+
for path in input_dir.rglob("*"):
|
|
22
|
+
if path.is_symlink():
|
|
23
|
+
raise ValueError(f"Refusing to pack symlink: {path}")
|
|
24
|
+
try:
|
|
25
|
+
path.resolve(strict=True).relative_to(root)
|
|
26
|
+
except (OSError, ValueError):
|
|
27
|
+
raise ValueError(f"Refusing to pack path outside input directory: {path}") from None
|
|
28
|
+
|
|
29
|
+
|
|
19
30
|
def main():
|
|
20
31
|
parser = argparse.ArgumentParser(description="Pack a directory into an Office file")
|
|
21
32
|
parser.add_argument("input_directory", help="Unpacked Office document directory")
|
|
@@ -60,6 +71,7 @@ def pack_document(input_dir, output_file, validate=False):
|
|
|
60
71
|
raise ValueError(f"{input_dir} is not a directory")
|
|
61
72
|
if output_file.suffix.lower() not in {".docx", ".pptx", ".xlsx"}:
|
|
62
73
|
raise ValueError(f"{output_file} must be a .docx, .pptx, or .xlsx file")
|
|
74
|
+
validate_input_tree(input_dir)
|
|
63
75
|
|
|
64
76
|
# Work in temporary directory to avoid modifying original
|
|
65
77
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
@@ -8,6 +8,11 @@ import sys
|
|
|
8
8
|
import zipfile
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
+
MAX_ARCHIVE_MEMBERS = 5000
|
|
12
|
+
MAX_MEMBER_SIZE = 100 * 1024 * 1024
|
|
13
|
+
MAX_TOTAL_UNCOMPRESSED = 512 * 1024 * 1024
|
|
14
|
+
MAX_COMPRESSION_RATIO = 1000
|
|
15
|
+
|
|
11
16
|
|
|
12
17
|
def _is_zip_symlink(member: zipfile.ZipInfo) -> bool:
|
|
13
18
|
return stat.S_ISLNK(member.external_attr >> 16)
|
|
@@ -29,19 +34,35 @@ def _extract_member(archive: zipfile.ZipFile, member: zipfile.ZipInfo, output_ro
|
|
|
29
34
|
shutil.copyfileobj(source, target)
|
|
30
35
|
|
|
31
36
|
|
|
37
|
+
def _validate_archive_members(archive: zipfile.ZipFile, output_root: Path):
|
|
38
|
+
members = archive.infolist()
|
|
39
|
+
if len(members) > MAX_ARCHIVE_MEMBERS:
|
|
40
|
+
raise ValueError("Archive contains too many entries")
|
|
41
|
+
|
|
42
|
+
total_size = 0
|
|
43
|
+
for member in members:
|
|
44
|
+
if _is_zip_symlink(member):
|
|
45
|
+
raise ValueError(f"Unsafe archive entry: {member.filename}")
|
|
46
|
+
if not _is_safe_destination(output_root, member.filename):
|
|
47
|
+
raise ValueError(f"Unsafe archive entry: {member.filename}")
|
|
48
|
+
if member.file_size > MAX_MEMBER_SIZE:
|
|
49
|
+
raise ValueError(f"Archive entry too large: {member.filename}")
|
|
50
|
+
total_size += member.file_size
|
|
51
|
+
if total_size > MAX_TOTAL_UNCOMPRESSED:
|
|
52
|
+
raise ValueError("Archive uncompressed size is too large")
|
|
53
|
+
if member.compress_size and member.file_size / member.compress_size > MAX_COMPRESSION_RATIO:
|
|
54
|
+
raise ValueError(f"Archive entry compression ratio too high: {member.filename}")
|
|
55
|
+
|
|
56
|
+
return members
|
|
57
|
+
|
|
58
|
+
|
|
32
59
|
def extract_archive_safely(input_file: str | Path, output_dir: str | Path):
|
|
33
60
|
output_path = Path(output_dir)
|
|
34
61
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
35
62
|
output_root = output_path.resolve()
|
|
36
63
|
|
|
37
64
|
with zipfile.ZipFile(input_file) as archive:
|
|
38
|
-
for member in archive
|
|
39
|
-
if _is_zip_symlink(member):
|
|
40
|
-
raise ValueError(f"Unsafe archive entry: {member.filename}")
|
|
41
|
-
if not _is_safe_destination(output_root, member.filename):
|
|
42
|
-
raise ValueError(f"Unsafe archive entry: {member.filename}")
|
|
43
|
-
|
|
44
|
-
for member in archive.infolist():
|
|
65
|
+
for member in _validate_archive_members(archive, output_root):
|
|
45
66
|
_extract_member(archive, member, output_path)
|
|
46
67
|
|
|
47
68
|
|
|
@@ -9,6 +9,14 @@ from pathlib import Path
|
|
|
9
9
|
import lxml.etree
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def hardened_xml_parser():
|
|
13
|
+
return lxml.etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False, huge_tree=False)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_xml(source, **kwargs):
|
|
17
|
+
return lxml.etree.parse(source, parser=hardened_xml_parser(), **kwargs)
|
|
18
|
+
|
|
19
|
+
|
|
12
20
|
def safe_extract_all(zip_ref, destination):
|
|
13
21
|
"""Extract a zip archive without allowing members to escape destination."""
|
|
14
22
|
destination = Path(destination).resolve()
|
|
@@ -149,7 +157,7 @@ class BaseSchemaValidator:
|
|
|
149
157
|
for xml_file in self.xml_files:
|
|
150
158
|
try:
|
|
151
159
|
# Try to parse the XML file
|
|
152
|
-
|
|
160
|
+
parse_xml(str(xml_file))
|
|
153
161
|
except lxml.etree.XMLSyntaxError as e:
|
|
154
162
|
errors.append(
|
|
155
163
|
f" {xml_file.relative_to(self.unpacked_dir)}: "
|
|
@@ -177,7 +185,7 @@ class BaseSchemaValidator:
|
|
|
177
185
|
|
|
178
186
|
for xml_file in self.xml_files:
|
|
179
187
|
try:
|
|
180
|
-
root =
|
|
188
|
+
root = parse_xml(str(xml_file)).getroot()
|
|
181
189
|
declared = set(root.nsmap.keys()) - {None} # Exclude default namespace
|
|
182
190
|
|
|
183
191
|
for attr_val in [
|
|
@@ -208,7 +216,7 @@ class BaseSchemaValidator:
|
|
|
208
216
|
|
|
209
217
|
for xml_file in self.xml_files:
|
|
210
218
|
try:
|
|
211
|
-
root =
|
|
219
|
+
root = parse_xml(str(xml_file)).getroot()
|
|
212
220
|
file_ids = {} # Track IDs that must be unique within this file
|
|
213
221
|
|
|
214
222
|
# Remove all mc:AlternateContent elements from the tree
|
|
@@ -328,7 +336,7 @@ class BaseSchemaValidator:
|
|
|
328
336
|
for rels_file in rels_files:
|
|
329
337
|
try:
|
|
330
338
|
# Parse relationships file
|
|
331
|
-
rels_root =
|
|
339
|
+
rels_root = parse_xml(str(rels_file)).getroot()
|
|
332
340
|
|
|
333
341
|
# Get the directory where this .rels file is located
|
|
334
342
|
rels_dir = rels_file.parent
|
|
@@ -429,7 +437,7 @@ class BaseSchemaValidator:
|
|
|
429
437
|
|
|
430
438
|
try:
|
|
431
439
|
# Parse the .rels file to get valid relationship IDs and their types
|
|
432
|
-
rels_root =
|
|
440
|
+
rels_root = parse_xml(str(rels_file)).getroot()
|
|
433
441
|
rid_to_type = {}
|
|
434
442
|
|
|
435
443
|
for rel in rels_root.findall(
|
|
@@ -452,7 +460,7 @@ class BaseSchemaValidator:
|
|
|
452
460
|
rid_to_type[rid] = type_name
|
|
453
461
|
|
|
454
462
|
# Parse the XML file to find all r:id references
|
|
455
|
-
xml_root =
|
|
463
|
+
xml_root = parse_xml(str(xml_file)).getroot()
|
|
456
464
|
|
|
457
465
|
# Find all elements with r:id attributes
|
|
458
466
|
for elem in xml_root.iter():
|
|
@@ -549,7 +557,7 @@ class BaseSchemaValidator:
|
|
|
549
557
|
|
|
550
558
|
try:
|
|
551
559
|
# Parse and get all declared parts and extensions
|
|
552
|
-
root =
|
|
560
|
+
root = parse_xml(str(content_types_file)).getroot()
|
|
553
561
|
declared_parts = set()
|
|
554
562
|
declared_extensions = set()
|
|
555
563
|
|
|
@@ -611,7 +619,7 @@ class BaseSchemaValidator:
|
|
|
611
619
|
continue
|
|
612
620
|
|
|
613
621
|
try:
|
|
614
|
-
root_tag =
|
|
622
|
+
root_tag = parse_xml(str(xml_file)).getroot().tag
|
|
615
623
|
root_name = root_tag.split("}")[-1] if "}" in root_tag else root_tag
|
|
616
624
|
|
|
617
625
|
if root_name in declarable_roots and path_str not in declared_parts:
|
|
@@ -850,15 +858,12 @@ class BaseSchemaValidator:
|
|
|
850
858
|
try:
|
|
851
859
|
# Load schema
|
|
852
860
|
with open(schema_path, "rb") as xsd_file:
|
|
853
|
-
|
|
854
|
-
xsd_doc = lxml.etree.parse(
|
|
855
|
-
xsd_file, parser=parser, base_url=str(schema_path)
|
|
856
|
-
)
|
|
861
|
+
xsd_doc = parse_xml(xsd_file, base_url=str(schema_path))
|
|
857
862
|
schema = lxml.etree.XMLSchema(xsd_doc)
|
|
858
863
|
|
|
859
864
|
# Load and preprocess XML
|
|
860
865
|
with open(xml_file, "r") as f:
|
|
861
|
-
xml_doc =
|
|
866
|
+
xml_doc = parse_xml(f)
|
|
862
867
|
|
|
863
868
|
xml_doc, _ = self._remove_template_tags_from_text_nodes(xml_doc)
|
|
864
869
|
xml_doc = self._preprocess_for_mc_ignorable(xml_doc)
|
|
@@ -10,7 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
|
|
11
11
|
import lxml.etree
|
|
12
12
|
|
|
13
|
-
from .base import BaseSchemaValidator
|
|
13
|
+
from .base import BaseSchemaValidator, parse_xml
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def safe_extract_all(zip_ref, destination):
|
|
@@ -100,7 +100,7 @@ class DOCXSchemaValidator(BaseSchemaValidator):
|
|
|
100
100
|
continue
|
|
101
101
|
|
|
102
102
|
try:
|
|
103
|
-
root =
|
|
103
|
+
root = parse_xml(str(xml_file)).getroot()
|
|
104
104
|
|
|
105
105
|
# Find all w:t elements
|
|
106
106
|
for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"):
|
|
@@ -153,7 +153,7 @@ class DOCXSchemaValidator(BaseSchemaValidator):
|
|
|
153
153
|
continue
|
|
154
154
|
|
|
155
155
|
try:
|
|
156
|
-
root =
|
|
156
|
+
root = parse_xml(str(xml_file)).getroot()
|
|
157
157
|
|
|
158
158
|
# Find all w:t elements that are descendants of w:del elements
|
|
159
159
|
namespaces = {"w": self.WORD_2006_NAMESPACE}
|
|
@@ -199,7 +199,7 @@ class DOCXSchemaValidator(BaseSchemaValidator):
|
|
|
199
199
|
continue
|
|
200
200
|
|
|
201
201
|
try:
|
|
202
|
-
root =
|
|
202
|
+
root = parse_xml(str(xml_file)).getroot()
|
|
203
203
|
# Count all w:p elements
|
|
204
204
|
paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p")
|
|
205
205
|
count = len(paragraphs)
|
|
@@ -221,7 +221,7 @@ class DOCXSchemaValidator(BaseSchemaValidator):
|
|
|
221
221
|
|
|
222
222
|
# Parse document.xml
|
|
223
223
|
doc_xml_path = temp_dir + "/word/document.xml"
|
|
224
|
-
root =
|
|
224
|
+
root = parse_xml(doc_xml_path).getroot()
|
|
225
225
|
|
|
226
226
|
# Count all w:p elements
|
|
227
227
|
paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p")
|
|
@@ -244,7 +244,7 @@ class DOCXSchemaValidator(BaseSchemaValidator):
|
|
|
244
244
|
continue
|
|
245
245
|
|
|
246
246
|
try:
|
|
247
|
-
root =
|
|
247
|
+
root = parse_xml(str(xml_file)).getroot()
|
|
248
248
|
namespaces = {"w": self.WORD_2006_NAMESPACE}
|
|
249
249
|
|
|
250
250
|
# Find w:delText in w:ins that are NOT within w:del
|