AI가 생성한 코드가 오픈소스 라이선스를 베꼈는지 PR에서 탐지하는 오픈소스 라이선스 게이트
Project description
Provenire
English · 한국어
Catch AI-generated code that copied open-source licenses — right in the pull request.
provenire — Latin, "to originate." It traces where your code came from.
The problem
Copilot, Cursor, and Claude were trained on GPL and AGPL repositories.
They reproduce that code — sometimes verbatim, without attribution. The industry now calls this "AI license laundering."
If copyleft code slips into your codebase, you may be obligated to open-source your entire product. Companies have already had to rewrite whole codebases because of this.
And yet every tool that detects it — FOSSA, Snyk, Black Duck — is commercial. Students, individuals, and small teams are left defenseless.
Provenire opens that door.
Worse: most license tools cannot even see this problem
What people call "open-source license scanning" is really three different jobs:
| Representative tools | What it inspects | Catches AI-copied code | |
|---|---|---|---|
| A. Dependency scanning (SCA) | FOSSA · Snyk · ORT · pip-licenses | the declared package list (requirements.txt, …) |
❌ |
| B. License-text scanners | ScanCode · licensee · FOSSology · REUSE | the license header in each file | ❌ |
| C. Code snippet matching | Black Duck · SCANOSS · Provenire | the similarity of the code itself | ⭕ |
When an LLM reproduces GPL code, nothing is added to your dependency list and no license header comes with it. A and B have nothing to look at. Only C can catch this.
Existing tools check what you pulled in. Provenire checks what you copied.
The key insight
GitHub Copilot's public code filter only blocks matches that are nearly character-identical.
Rename the variables, and it sails right through.
Provenire anonymizes identifiers before fingerprinting:
original: def elide_filename(filename, length): -> def ID(ID, ID):
AI output: def truncate_path(path_str, max_len): -> def ID(ID, ID):
^ the fingerprints become identical
Erase the names. Keep the structure (keywords, operators, control flow). Then fingerprint it.
→ No matter how the names change, we still catch it.
And even within group C, Provenire is currently the only tool that erases identifiers:
| Normalization | k-gram | Identifiers | |
|---|---|---|---|
| MOSS (standard config) | strip comments & whitespace | 5 characters | kept |
| SCANOSS | strip non-alphanumeric characters | 30 characters | kept |
| Provenire | strip comments + identifiers → ID |
15 tokens | erased |
If names survive into the fingerprint, renaming breaks the fingerprint.
It actually works
Suppose an AI reproduced GPL code with only the variable names changed:
$ provenire compare ai_output.py gpl_origin.py
mode similarity fingerprints
---------------------------------------------
raw (baseline) 12.0% 3/25 <- Copilot-filter level: below threshold, MISSED
tokens (default) 100.0% 24/24 <- Provenire: CAUGHT
---------------------------------------------
[!] Suspected copy — 100.0% similarity (threshold 30%)
Exit code 1 → drop it straight into CI as a PR gate.
Verified results
Benchmarked against real GPL-3.0 code (qutebrowser/utils/utils.py).
| Transformation | raw (baseline) | tokens (Provenire) |
|---|---|---|
| Verbatim copy | 100% | 100% |
| Comments & docstrings stripped | 100% | 87.5% |
| All variable & function names renamed | 8.8% (missed) | 100.0% (caught) |
| Renamed + stripped + reformatted | 8.8% (missed) | 87.5% (caught) |
| Unrelated code (negative control) | 0% | 0% (no false positive) |
The raw engine stays well under the 30% threshold once names change (missed), while the token engine catches 100% with 0% false positives.
Beyond single functions
Provenire chunks code function by function, so it catches copyleft even when it is buried in a
larger file — a whole GPL file pasted in, or a single GPL function renamed and mixed into your own
code. On a 9-project copyleft corpus: Precision 100% · Recall 90.7% · F1 95.1% · 0 false positives
(python benchmarks/evaluate.py).
Reproduce it yourself:
benchmarks/· Details:benchmarks/RESULTS.md
Where this sits in the research
Provenance tracking for LLM-generated code is an active research area. Recent work (Gurioli et al., Efficient and Scalable Provenance Tracking for LLM-Generated Code Snippets, 2026) solves winnowing's scalability problem — a vector-search first stage takes a 10M-snippet corpus from linear- to logarithmic-time retrieval. That work, however, keeps identifiers during normalization, and models renaming as "a 20% probability of replacing words longer than three characters that appear more than twice" — so most identifiers survive verbatim.
Provenire pushes on an orthogonal axis: not scale, but robustness to transformation —
detection when every identifier has been changed. Notably, the semantic chunking that paper
lists as future work ("semantically meaningful units, such as function bodies … via AST analysis")
is already implemented here
(index/chunker.py).
Conversely, the Type-3 clones it leaves open (inserted, deleted, or reordered statements) remain
open for us too — see the roadmap.
Install
pip install provenire
Usage
provenire compare <suspect> <origin> # similarity between two files
provenire fingerprint <file> # preview the fingerprint
provenire scan <path> # scan files — bundled copyleft index, works out of the box
provenire scan --diff <ref> # scan only the code added since <ref> (PR gate)
provenire scan <path> --index <db> # ...or point at your own fingerprint DB
from provenire import compare
m = compare(ai_generated_code, gpl_source)
if m.is_suspicious:
print(f"Suspected copy: {m.similarity:.1%}")
# other languages
compare(java_a, java_b, lang="java")
GitHub Action
Gate every pull request automatically. Provenire scans only the code added in the PR
(scan --diff), comments on the PR when a copyleft match is found, and can fail the check.
# .github/workflows/provenire.yml
name: Provenire
on: pull_request
permissions:
contents: read
pull-requests: write # required to comment on the PR
jobs:
license-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required — the base commit must exist for the diff
- uses: 4thIS/opensource_contest_provenire/.github/action@v0.1
with:
fail-on: true # fail the check on a match (false = comment only)
A match is reported as file:start-end plus the name of your enclosing function, so you know
exactly which lines to look at:
⚠️ Provenire — copyleft-similar code found
98.2% src/utils.py:41-70 (sanitize_path)
↳ qutebrowser/utils.py :: sanitize_filename [GPL-3.0-or-later]
Line numbers refer to the original file even under --diff, which only scans added lines.
The copyleft fingerprint index ships inside the package, so it works with no extra setup.
Point at your own DB with index: path/to/your.db if you build one.
Notes:
fetch-depth: 0is required. The default shallow checkout has no base commit, so--diffcannot compute the added lines.- Fork PRs run with a read-only token, so the comment step is skipped; the failing check still signals the problem.
- Start with
fail-on: false. Let it comment for a week before it blocks anyone — that is how we found (and fixed) a false-positive class in our own repo.
Roadmap
- Winnowing fingerprint engine
- Token normalization (identifier anonymization) — the core moat
- Similarity judgment (containment)
- CLI (
compare/fingerprint) - Language packs — Python · Java · JavaScript · TypeScript
- Bundled copyleft index — ships with the package;
scanworks out of the box -
provenire scan— file scan (function-level chunking) &--diffPR gate - GitHub Action — PR comment + failing check (usage)
- Scale the index (LSH / MinHash) for larger corpora
- LLM second-pass judgment (idiom vs. structural reproduction)
- More languages (Go, C++, …) & function-level chunking beyond Python
- pre-commit hook
How it works
PR diff (added lines)
|
|-- 1. Normalize strip comments -> tokenize -> anonymize identifiers to ID
|-- 2. Fingerprint k-gram hashes -> sliding-window minimum (winnowing)
|-- 3. Index lookup query the copyleft fingerprint database
|-- 4. Judge containment = |shared fingerprints| / |suspect fingerprints|
`-- 5. Report origin link · license · risk level
Algorithm: Schleimer, Wilkerson & Aiken, "Winnowing: Local Algorithms for Document Fingerprinting" (SIGMOD 2003) — the basis of MOSS.
Adding a language
Language packs are one file. You never touch the core.
# src/provenire/languages/java.py
from pygments.token import Token
from .base import LanguageSpec
SPEC = LanguageSpec(
name="java",
lexer="java",
extensions=(".java",),
keep=(Token.Keyword, Token.Keyword.Type), # types are structure
)
Register it in languages/__init__.py, add a test, done.
See CONTRIBUTING.md — good first issue welcome.
Contributing
The most valuable contributions right now:
- New language packs (Java, Go, Rust, JS/TS) —
good first issue - False-positive reports — boilerplate that gets wrongly flagged
- Benchmark cases — especially code an LLM actually regenerated
See CONTRIBUTING.md.
License
Apache-2.0 — patent grant included. Use it freely; keep the attribution.
Provenire is not legal advice. It flags suspicious regions. It does not render a verdict.
Built for the 2026 Open Source Developer Contest (Korea) · Team 코드감식반 (Lee Woojin · Kim Minsu)
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file provenire-0.1.4.tar.gz.
File metadata
- Download URL: provenire-0.1.4.tar.gz
- Upload date:
- Size: 256.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1c626839e2ab06b0a9a40820a415e16ee617e63468ff1de3f94831fd82d9d58a
|
|
| MD5 |
e9e1105065f9a20cce0a19a7f922b94e
|
|
| BLAKE2b-256 |
d592e56a8b5e16ede9f571b9e99e47046810a38e173f8bdeb97a772182d7e846
|
File details
Details for the file provenire-0.1.4-py3-none-any.whl.
File metadata
- Download URL: provenire-0.1.4-py3-none-any.whl
- Upload date:
- Size: 225.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66f531f2cf90955a83bb0c4f350c258d93ec2b1bd2925ed194d3bd4b051bbbc3
|
|
| MD5 |
3fb81fafbda40190571a260757c49bfb
|
|
| BLAKE2b-256 |
b6bb37649d6ae5533f0fd2b7b1582f8064e0d32ef15774e816d270b1dea76555
|