Skip to content

Commit d8ebedc

Browse files
committed
Initial commit..
0 parents  commit d8ebedc

29 files changed

Lines changed: 2688 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
name: test (py${{ matrix.python-version }})
12+
runs-on: ubuntu-latest
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
python-version: ["3.10", "3.11", "3.12", "3.13"]
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Set up Python ${{ matrix.python-version }}
22+
uses: actions/setup-python@v5
23+
with:
24+
python-version: ${{ matrix.python-version }}
25+
26+
- name: Install
27+
run: |
28+
python -m pip install --upgrade pip
29+
pip install -e '.[dev]'
30+
31+
- name: Lint
32+
run: ruff check
33+
34+
- name: Test
35+
run: pytest -v
36+
37+
docker:
38+
name: docker build
39+
runs-on: ubuntu-latest
40+
steps:
41+
- uses: actions/checkout@v4
42+
43+
- name: Build image
44+
run: docker build -t glassglyph-scanner:ci .
45+
46+
- name: Run health check
47+
run: |
48+
docker run -d --name glassglyph -p 8080:8080 glassglyph-scanner:ci
49+
for i in {1..30}; do
50+
if curl -sf http://localhost:8080/health > /dev/null; then
51+
echo "service healthy"
52+
exit 0
53+
fi
54+
sleep 1
55+
done
56+
echo "service did not become healthy"
57+
docker logs glassglyph
58+
exit 1

.github/workflows/docs.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Deploy docs
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "docs/**"
8+
- "mkdocs.yml"
9+
- ".github/workflows/docs.yml"
10+
11+
permissions:
12+
contents: write
13+
14+
jobs:
15+
deploy:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- uses: actions/setup-python@v5
21+
with:
22+
python-version: "3.12"
23+
24+
- name: Install docs dependencies
25+
run: pip install mkdocs>=1.6 mkdocs-material>=9.5
26+
27+
- name: Build and deploy
28+
run: mkdocs gh-deploy --force --clean

.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
__pycache__/
2+
*.py[cod]
3+
*$py.class
4+
*.egg-info/
5+
.eggs/
6+
dist/
7+
build/
8+
.pytest_cache/
9+
.ruff_cache/
10+
.venv/
11+
venv/
12+
.env
13+
.coverage
14+
htmlcov/
15+
*.log
16+
.DS_Store
17+
site/
18+
.pytest_cache/

Dockerfile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
FROM python:3.12-slim
2+
3+
WORKDIR /app
4+
5+
# Install build deps first so this layer caches well
6+
COPY pyproject.toml README.md LICENSE ./
7+
COPY src/ ./src/
8+
9+
RUN pip install --no-cache-dir '.[server]'
10+
11+
# Run as non-root
12+
RUN useradd --system --uid 1000 glassglyph-scanner && chown -R glassglyph-scanner /app
13+
USER glassglyph-scanner
14+
15+
EXPOSE 8080
16+
17+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
18+
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health', timeout=2).read()"
19+
20+
CMD ["uvicorn", "glassglyph-scanner.server:app", "--host", "0.0.0.0", "--port", "8080"]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Jim Pringle
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# Glassglyph Scanner
2+
3+
**A reference scanner for invisible unicode and homoglyph attacks on text-based systems.**
4+
5+
In March 2026, the Glassworm campaign compromised 151+ GitHub repositories, npm packages, and VS Code extensions by smuggling malicious code inside invisible Unicode characters — text that renders as zero pixels in every editor, terminal, code review tool, and browser. The same class of attack works against RAG pipelines, LLM agents, email, chat, and any system that ingests text and later retrieves it as context.
6+
7+
Glassglyph Scanner scans text for three attack classes:
8+
9+
1. **Invisible unicode encoding** — Glassworm's substitution cipher (variation selectors U+FE00–FE0F and U+E0100–E01EF) plus tag characters that map 1:1 to printable ASCII.
10+
2. **Bidi override attacks** — control characters that visually reorder text (`safe.txt``safe[RLO]txt.exe`).
11+
3. **Homoglyph substitution** — Cyrillic and Greek characters replacing visually identical Latin letters (`аnthropic.com` with Cyrillic `а`).
12+
13+
Cost: **under a millisecond per document**. Pure string iteration, zero external dependencies in the core library, zero network I/O, zero LLM inference.
14+
15+
---
16+
17+
## Try it in 30 seconds
18+
19+
**Option 1 — Docker:**
20+
21+
```bash
22+
git clone https://github.com/pringlized/glassglyph-scanner.git
23+
cd glassglyph-scanner
24+
docker compose up -d
25+
curl -X POST http://localhost:8080/scan \
26+
-H 'Content-Type: application/json' \
27+
-d '{"content":"visit dоcs.аnthropic.com for the API"}'
28+
```
29+
30+
Expected response: the scanner flags the Cyrillic `о` and `а` as a medium-severity homoglyph finding.
31+
32+
**Option 2 — Python:**
33+
34+
```bash
35+
pip install -e '.[all]'
36+
python -c "from glassglyph_scanner import sanitize; r = sanitize('visit dоcs.аnthropic.com'); print(r.findings[0])"
37+
```
38+
39+
**Option 3 — CLI:**
40+
41+
```bash
42+
pip install -e '.[cli]'
43+
echo 'visit dоcs.аnthropic.com' | glassglyph-scanner scan -
44+
# exit 1 → findings present (flagged, not blocked)
45+
46+
echo 'plain text' | glassglyph-scanner scan -
47+
# exit 0 → clean
48+
```
49+
50+
---
51+
52+
## Why this matters
53+
54+
Traditional supply-chain attacks need a decoder at the execution site. **RAG pipelines are different — the LLM is both target and decoder.** When a poisoned item is retrieved as agent context:
55+
56+
1. The agent reads it as knowledge
57+
2. LLMs tokenize at the byte level — invisible characters are not invisible to the model
58+
3. The model may follow instructions encoded in them
59+
4. The item was embedded and clustered with legitimate knowledge, so it has full semantic credibility
60+
61+
**Ingestion-time scanning is the only viable enforcement point.** Once the content is embedded, it's semantically indistinguishable from clean knowledge.
62+
63+
See [`docs/threat-model.md`](docs/threat-model.md) for the full threat model.
64+
65+
---
66+
67+
## Detection rules
68+
69+
| Range | Name | Severity | Action |
70+
|---|---|---|---|
71+
| `U+FE00``U+FE0F` | Variation selectors | **Critical** | Block |
72+
| `U+E0100``U+E01EF` | Supp. variation selectors | **Critical** | Block |
73+
| `U+E0020``U+E007F` | Tag characters | **Critical** | Block |
74+
| `U+200B``U+200F` | Zero-width / bidi marks | High | Strip |
75+
| `U+202A``U+202E` | Bidi overrides | High | Strip |
76+
| `U+2060``U+2064` | Invisible math operators | High | Strip |
77+
| `U+FEFF` (non-BOM position) | Zero-width no-break space | High | Strip |
78+
| `U+E0001` | Language tag (deprecated) | High | Strip |
79+
| Mixed-script word w/ confusable | e.g. Cyrillic `а` in `аnthropic` | Medium | Flag |
80+
| Mixed-script word w/o confusable | e.g. Cyrillic `ж` in `aжb` | Low | Flag |
81+
82+
**Critical = block:** no legitimate text contains these ranges. Their presence indicates an encoding attack. Reject the document.
83+
84+
**High = strip:** these characters have narrow legitimate uses (Arabic text shaping, emoji sequences) but are dangerous in knowledge items. Characters are removed, the document proceeds with sanitized text.
85+
86+
**Medium/Low = flag:** homoglyphs can appear in legitimate multilingual content. The finding is reported; the calling system decides whether to quarantine.
87+
88+
Full rule reference in [`docs/detection-rules.md`](docs/detection-rules.md).
89+
90+
---
91+
92+
## Library API
93+
94+
```python
95+
from glassglyph_scanner import sanitize
96+
97+
result = sanitize("some text")
98+
99+
if result.has_critical_findings:
100+
# BLOCK — invisible encoding detected
101+
log_and_reject(result.findings)
102+
elif result.was_modified:
103+
# STRIP — use sanitized content going forward
104+
process(result.sanitized_content)
105+
elif result.findings:
106+
# FLAG — content unmodified, review findings
107+
queue_for_review(result.findings)
108+
else:
109+
# CLEAN — proceed
110+
process(result.sanitized_content)
111+
```
112+
113+
`SanitizationResult` fields:
114+
115+
- `clean: bool` — true iff no findings
116+
- `sanitized_content: str` — content with high-severity chars removed
117+
- `findings: list[SanitizationFinding]` — each with `threat_category`, `severity`, `description`, `character_ranges`, `action_taken`
118+
- `has_critical_findings: bool` — signal to block
119+
- `was_modified: bool` — true iff sanitized_content differs from input
120+
- `scan_duration_ms: float`
121+
122+
---
123+
124+
## HTTP API
125+
126+
```
127+
POST /scan body: {"content": "..."}
128+
GET /health liveness probe
129+
GET / landing page
130+
GET /docs OpenAPI UI
131+
```
132+
133+
Full reference: [`docs/api.md`](docs/api.md)
134+
135+
---
136+
137+
## CLI
138+
139+
```bash
140+
glassglyph-scanner scan FILE # scan a file
141+
glassglyph-scanner scan - # scan stdin
142+
glassglyph-scanner scan FILE --json # machine-readable output
143+
glassglyph-scanner scan FILE --quiet # exit code only
144+
glassglyph-scanner --version
145+
```
146+
147+
Exit codes:
148+
- `0` — clean
149+
- `1` — findings present (stripped or flagged)
150+
- `2` — critical findings (block)
151+
- `64` — usage error
152+
153+
---
154+
155+
## Installation
156+
157+
```bash
158+
# Core library only (zero dependencies)
159+
pip install glassglyph-scanner
160+
161+
# With CLI
162+
pip install 'glassglyph-scanner[cli]'
163+
164+
# With HTTP service
165+
pip install 'glassglyph-scanner[server]'
166+
167+
# Everything
168+
pip install 'glassglyph-scanner[all]'
169+
170+
# Development (tests, linting)
171+
pip install -e '.[dev]'
172+
```
173+
174+
---
175+
176+
## Examples
177+
178+
The `examples/` directory contains:
179+
180+
- `clean.txt` — normal content
181+
- `glassworm_attack.txt` — a pre-built Glassworm-encoded payload
182+
- `homoglyph_url_spoof.txt` — URL with Cyrillic substitutions
183+
- `zero_width_strip.txt` — zero-width chars interspersed in visible text
184+
- `generate_glassworm.py` — generate your own invisible payloads
185+
186+
Run all examples:
187+
188+
```bash
189+
for f in examples/*.txt; do
190+
echo "=== $f ==="
191+
glassglyph-scanner scan "$f"
192+
done
193+
```
194+
195+
---
196+
197+
## What this is NOT
198+
199+
- **Not a semantic/intent scanner.** Glassglyph Scanner does character-level detection only. Attacks using natural-language prompt injection in plain ASCII are outside its scope — that class of attack requires LLM inference to detect.
200+
- **Not a content filter.** This scans for encoding-based attacks, not for policy violations, PII, or toxic content.
201+
- **Not a replacement for TLS, authentication, rate limiting, or other perimeter controls.**
202+
203+
For the full two-gate defense model (character gate + intent gate), see `docs/threat-model.md`.
204+
205+
---
206+
207+
## Development
208+
209+
```bash
210+
git clone https://github.com/pringlized/glassglyph-scanner.git
211+
cd glassglyph-scanner
212+
pip install -e '.[dev]'
213+
pytest # run all tests
214+
ruff check # lint
215+
uvicorn glassglyph_scanner.server:app --reload # serve locally
216+
```
217+
218+
---
219+
220+
## License
221+
222+
MIT. See `LICENSE`.
223+
224+
---
225+
226+
## Credits
227+
228+
Research basis: Aikido Security's March 2026 Glassworm writeup, Unicode Consortium TR39 confusables data, and community security research on invisible-unicode supply-chain attacks.
229+
230+
Built as a reference implementation from the Mens Altera ingestion pipeline's Gate 1 character sanitization layer.

docker-compose.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
services:
2+
glassglyph-scanner:
3+
build: .
4+
container_name: glassglyph-scanner
5+
ports:
6+
- "8080:8080"
7+
restart: unless-stopped
8+
# Read-only root FS since we don't write anything
9+
read_only: true
10+
# Drop all capabilities — the service doesn't need them
11+
cap_drop:
12+
- ALL
13+
security_opt:
14+
- no-new-privileges:true

0 commit comments

Comments
 (0)