ghostcode 0.5.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.
- ghostcode/__init__.py +3 -0
- ghostcode/audit/__init__.py +0 -0
- ghostcode/audit/logger.py +149 -0
- ghostcode/cli.py +986 -0
- ghostcode/config.py +187 -0
- ghostcode/mapping/__init__.py +0 -0
- ghostcode/mapping/encryption.py +143 -0
- ghostcode/mapping/ghost_map.py +222 -0
- ghostcode/mapping/token_generator.py +78 -0
- ghostcode/parsers/__init__.py +0 -0
- ghostcode/parsers/base.py +66 -0
- ghostcode/parsers/cpp_parser.py +341 -0
- ghostcode/parsers/python_parser.py +397 -0
- ghostcode/reveal/__init__.py +0 -0
- ghostcode/reveal/code_revealer.py +374 -0
- ghostcode/reveal/diff_analyzer.py +426 -0
- ghostcode/reveal/explanation_translator.py +214 -0
- ghostcode/risk_report.py +467 -0
- ghostcode/transformers/__init__.py +0 -0
- ghostcode/transformers/comment_anonymizer.py +95 -0
- ghostcode/transformers/comment_stripper.py +60 -0
- ghostcode/transformers/isolator.py +312 -0
- ghostcode/transformers/literal_scrubber.py +452 -0
- ghostcode/transformers/multi_file.py +99 -0
- ghostcode/transformers/symbol_renamer.py +64 -0
- ghostcode/utils/__init__.py +0 -0
- ghostcode/utils/clipboard.py +52 -0
- ghostcode/utils/stdlib_registry.py +221 -0
- ghostcode-0.5.0.dist-info/METADATA +92 -0
- ghostcode-0.5.0.dist-info/RECORD +33 -0
- ghostcode-0.5.0.dist-info/WHEEL +5 -0
- ghostcode-0.5.0.dist-info/entry_points.txt +2 -0
- ghostcode-0.5.0.dist-info/top_level.txt +1 -0
ghostcode/cli.py
ADDED
|
@@ -0,0 +1,986 @@
|
|
|
1
|
+
"""GhostCode CLI — Privacy Proxy for Developers.
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
ghost hide [files] Strip business context, produce anonymous code
|
|
5
|
+
ghost reveal [file] Restore original names from AI's response
|
|
6
|
+
ghost map [mapfile] Inspect a ghost map
|
|
7
|
+
ghost status Show GhostCode status and recent activity
|
|
8
|
+
ghost demo Run interactive demo (hackathon)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
from . import __version__
|
|
20
|
+
from .audit.logger import AuditLogger
|
|
21
|
+
from .config import load_config
|
|
22
|
+
from .mapping.ghost_map import GhostMap
|
|
23
|
+
from .parsers.base import ParseResult
|
|
24
|
+
from .parsers.cpp_parser import CppParser
|
|
25
|
+
from .parsers.python_parser import PythonParser
|
|
26
|
+
from .transformers.comment_anonymizer import CommentAnonymizer
|
|
27
|
+
from .transformers.comment_stripper import CommentStripper
|
|
28
|
+
from .transformers.literal_scrubber import LiteralScrubber
|
|
29
|
+
from .transformers.symbol_renamer import SymbolRenamer
|
|
30
|
+
from .transformers.isolator import CppIsolator, PythonIsolator
|
|
31
|
+
from .transformers.multi_file import process_multiple_files
|
|
32
|
+
from .utils.clipboard import copy_to_clipboard, clipboard_available
|
|
33
|
+
from .risk_report import RiskAnalyzer, format_risk_report_cli
|
|
34
|
+
|
|
35
|
+
LANGUAGE_MAP = {
|
|
36
|
+
".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp",
|
|
37
|
+
".c": "cpp", ".h": "cpp", ".hpp": "cpp", ".hxx": "cpp",
|
|
38
|
+
".py": "python",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# ── Styled output helpers ───────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
BANNER = r"""
|
|
44
|
+
________ __ ______ __
|
|
45
|
+
/ ____/ /_ ____ _____/ /_/ ____/___ ____/ /__
|
|
46
|
+
/ / __/ __ \/ __ \/ ___/ __/ / / __ \/ __ / _ \
|
|
47
|
+
/ /_/ / / / / /_/ (__ ) /_/ /___/ /_/ / /_/ / __/
|
|
48
|
+
\____/_/ /_/\____/____/\__/\____/\____/\__,_/\___/
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _header(text: str):
|
|
53
|
+
"""Print a styled section header."""
|
|
54
|
+
click.secho(f"\n{'=' * 54}", fg="cyan")
|
|
55
|
+
click.secho(f" {text}", fg="cyan", bold=True)
|
|
56
|
+
click.secho(f"{'=' * 54}", fg="cyan")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _step(label: str, detail: str = "", done: bool = True):
|
|
60
|
+
"""Print a step indicator."""
|
|
61
|
+
icon = click.style(" [+]", fg="green") if done else click.style(" [~]", fg="yellow")
|
|
62
|
+
msg = f"{icon} {label}"
|
|
63
|
+
if detail:
|
|
64
|
+
msg += click.style(f" {detail}", fg="white", dim=True)
|
|
65
|
+
click.echo(msg)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _warn(msg: str):
|
|
69
|
+
"""Print a warning."""
|
|
70
|
+
click.secho(f" [!] {msg}", fg="yellow")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _error(msg: str):
|
|
74
|
+
"""Print an error and exit."""
|
|
75
|
+
click.secho(f"\n ERROR: {msg}", fg="red", bold=True, err=True)
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _info(label: str, value: str):
|
|
80
|
+
"""Print an info line."""
|
|
81
|
+
click.echo(f" {click.style(label + ':', bold=True):30s} {value}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _divider():
|
|
85
|
+
click.secho(f" {'─' * 50}", fg="white", dim=True)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _get_language(file_path: str) -> str | None:
|
|
89
|
+
_, ext = os.path.splitext(file_path)
|
|
90
|
+
return LANGUAGE_MAP.get(ext.lower())
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _get_parser(language: str):
|
|
94
|
+
if language == "cpp":
|
|
95
|
+
return CppParser()
|
|
96
|
+
if language == "python":
|
|
97
|
+
return PythonParser()
|
|
98
|
+
_error(f"Unsupported language '{language}'. Supported: C/C++, Python")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _default_map_dir() -> str:
|
|
102
|
+
return os.path.join(".ghostcode", "maps")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _generate_map_path(source_file: str) -> str:
|
|
106
|
+
base = os.path.splitext(os.path.basename(source_file))[0]
|
|
107
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
108
|
+
return os.path.join(_default_map_dir(), f"{base}_{timestamp}.json")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── Main CLI group ──────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
@click.group()
|
|
114
|
+
@click.version_option(version=__version__, prog_name="ghostcode")
|
|
115
|
+
def main():
|
|
116
|
+
"""GhostCode — Privacy Proxy for Developers.
|
|
117
|
+
|
|
118
|
+
Strip business context from code before sending to AI.
|
|
119
|
+
Restore original names after getting AI's response.
|
|
120
|
+
|
|
121
|
+
Your code stays anonymous. Your logic stays intact.
|
|
122
|
+
"""
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ── HIDE command ────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
@main.command()
|
|
129
|
+
@click.argument("file_paths", nargs=-1, type=click.Path(exists=True), required=True)
|
|
130
|
+
@click.option("--level", type=click.IntRange(1, 4), default=None,
|
|
131
|
+
help="Privacy level: 1=names+comments, 2=+literals, 3=+isolation, 4=+dimensions")
|
|
132
|
+
@click.option("--output", "-o", type=click.Path(), default=None,
|
|
133
|
+
help="Output file path (default: ghost_<filename>)")
|
|
134
|
+
@click.option("--map-file", type=click.Path(), default=None,
|
|
135
|
+
help="Map file path (default: .ghostcode/maps/<name>_<timestamp>.json)")
|
|
136
|
+
@click.option("--function", "-f", type=str, default=None,
|
|
137
|
+
help="Function to isolate (Level 3+).")
|
|
138
|
+
@click.option("--encrypt/--no-encrypt", default=None,
|
|
139
|
+
help="Encrypt the map file (default: from config)")
|
|
140
|
+
@click.option("--passphrase", type=str, default=None, hide_input=True,
|
|
141
|
+
help="Passphrase for map encryption (prompted if not provided)")
|
|
142
|
+
@click.option("--copy/--no-copy", "clipboard", default=True,
|
|
143
|
+
help="Copy ghost output to clipboard (default: yes)")
|
|
144
|
+
@click.option("--keep-comments", is_flag=True, default=False,
|
|
145
|
+
help="Anonymize comments instead of stripping them")
|
|
146
|
+
@click.option("--risk-report/--no-risk-report", "show_risk_report", default=True,
|
|
147
|
+
help="Show pre-send risk report (default: yes)")
|
|
148
|
+
def hide(file_paths: tuple[str, ...], level: int | None, output: str | None,
|
|
149
|
+
map_file: str | None, function: str | None,
|
|
150
|
+
encrypt: bool | None, passphrase: str | None,
|
|
151
|
+
clipboard: bool, keep_comments: bool, show_risk_report: bool):
|
|
152
|
+
"""Strip business context from source file(s).
|
|
153
|
+
|
|
154
|
+
Privacy levels:
|
|
155
|
+
1 — Rename user symbols + strip comments
|
|
156
|
+
2 — + Scrub string/numeric literals (smart classification)
|
|
157
|
+
3 — + Function isolation with dependency stubs
|
|
158
|
+
4 — + Dimension generalization (loop bounds → constants)
|
|
159
|
+
|
|
160
|
+
Examples:
|
|
161
|
+
ghost hide app.py
|
|
162
|
+
ghost hide server.cpp --level 3 --function handle_request
|
|
163
|
+
ghost hide *.py --encrypt --level 4
|
|
164
|
+
"""
|
|
165
|
+
# Load config
|
|
166
|
+
config = load_config()
|
|
167
|
+
audit = AuditLogger(enabled=config.enforce_audit)
|
|
168
|
+
|
|
169
|
+
# Resolve level
|
|
170
|
+
if level is None:
|
|
171
|
+
level = config.default_scrub_level
|
|
172
|
+
try:
|
|
173
|
+
level = config.validate_level(level)
|
|
174
|
+
except ValueError as e:
|
|
175
|
+
_error(str(e))
|
|
176
|
+
|
|
177
|
+
# Resolve encryption
|
|
178
|
+
use_encryption = encrypt if encrypt is not None else config.encrypt_maps
|
|
179
|
+
|
|
180
|
+
# Process first file (multi-file uses the first for primary output)
|
|
181
|
+
file_path = file_paths[0]
|
|
182
|
+
|
|
183
|
+
# Check banned patterns
|
|
184
|
+
for fp in file_paths:
|
|
185
|
+
if config.check_banned(fp):
|
|
186
|
+
_error(
|
|
187
|
+
f"'{fp}' matches a banned pattern in repo policy.\n"
|
|
188
|
+
f" Banned patterns: {', '.join(config.banned_patterns)}\n"
|
|
189
|
+
f" Contact your security team for exceptions."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
language = _get_language(file_path)
|
|
193
|
+
if not language:
|
|
194
|
+
ext = os.path.splitext(file_path)[1]
|
|
195
|
+
_error(
|
|
196
|
+
f"Unsupported file type '{ext}'.\n"
|
|
197
|
+
f" Supported: .py, .cpp, .cc, .c, .h, .hpp"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# ── Header ─────────────────────────────────────────────────────
|
|
201
|
+
_header("GHOST HIDE")
|
|
202
|
+
level_labels = {
|
|
203
|
+
1: "Names + Comments",
|
|
204
|
+
2: "Names + Comments + Literals",
|
|
205
|
+
3: "Names + Comments + Literals + Isolation",
|
|
206
|
+
4: "Full (Names + Comments + Literals + Isolation + Dimensions)",
|
|
207
|
+
}
|
|
208
|
+
_info("Source", ", ".join(file_paths))
|
|
209
|
+
_info("Language", language.upper())
|
|
210
|
+
_info("Privacy Level", f"{level} — {level_labels[level]}")
|
|
211
|
+
if function:
|
|
212
|
+
_info("Isolate Function", function)
|
|
213
|
+
if use_encryption:
|
|
214
|
+
_info("Map Encryption", "ON")
|
|
215
|
+
click.echo()
|
|
216
|
+
|
|
217
|
+
# Read source
|
|
218
|
+
with open(file_path) as f:
|
|
219
|
+
source_code = f.read()
|
|
220
|
+
|
|
221
|
+
# ── Level 3+: Function Isolation ──────────────────────────────
|
|
222
|
+
isolated = False
|
|
223
|
+
if level >= 3 and function:
|
|
224
|
+
if language == "cpp":
|
|
225
|
+
isolator = CppIsolator()
|
|
226
|
+
else:
|
|
227
|
+
isolator = PythonIsolator()
|
|
228
|
+
|
|
229
|
+
isolated_source = isolator.isolate(source_code, function)
|
|
230
|
+
if isolated_source:
|
|
231
|
+
source_code = isolated_source
|
|
232
|
+
isolated = True
|
|
233
|
+
_step("Function isolated", f"'{function}' extracted with stubs")
|
|
234
|
+
else:
|
|
235
|
+
_warn(f"Function '{function}' not found — processing full file")
|
|
236
|
+
|
|
237
|
+
# ── Parse & Transform ─────────────────────────────────────────
|
|
238
|
+
parser = _get_parser(language)
|
|
239
|
+
ghost_map = GhostMap()
|
|
240
|
+
multi_mode = len(file_paths) > 1 and not isolated
|
|
241
|
+
|
|
242
|
+
if multi_mode:
|
|
243
|
+
# ── Multi-file: consistent tokens across all files ────────
|
|
244
|
+
_step("Multi-file mode", f"{len(file_paths)} files")
|
|
245
|
+
|
|
246
|
+
# Group files by language and process each group with correct parser
|
|
247
|
+
multi_results = []
|
|
248
|
+
lang_groups: dict[str, list[str]] = {}
|
|
249
|
+
for fp in file_paths:
|
|
250
|
+
lang = _get_language(fp) or language
|
|
251
|
+
lang_groups.setdefault(lang, []).append(fp)
|
|
252
|
+
|
|
253
|
+
for lang, group_files in lang_groups.items():
|
|
254
|
+
group_parser = _get_parser(lang)
|
|
255
|
+
group_results = process_multiple_files(
|
|
256
|
+
group_files, group_parser, ghost_map,
|
|
257
|
+
strip_comments=not keep_comments,
|
|
258
|
+
)
|
|
259
|
+
multi_results.extend(group_results)
|
|
260
|
+
|
|
261
|
+
_step("AST parsed & renamed",
|
|
262
|
+
f"{ghost_map.symbol_count} symbols across {len(file_paths)} files")
|
|
263
|
+
|
|
264
|
+
# Collect stats from all files
|
|
265
|
+
total_comments = sum(cc for _, _, _, cc in multi_results)
|
|
266
|
+
if keep_comments:
|
|
267
|
+
_step("Comments anonymized", f"{total_comments} anonymized")
|
|
268
|
+
else:
|
|
269
|
+
_step("Comments stripped", f"{total_comments} removed")
|
|
270
|
+
|
|
271
|
+
# Use first file as primary ghost output
|
|
272
|
+
ghost_source = multi_results[0][1]
|
|
273
|
+
comment_count = total_comments
|
|
274
|
+
|
|
275
|
+
# Write all files
|
|
276
|
+
all_outputs = []
|
|
277
|
+
for fpath, gsource, _, _ in multi_results:
|
|
278
|
+
oname = f"ghost_{os.path.basename(fpath)}"
|
|
279
|
+
with open(oname, "w") as f:
|
|
280
|
+
f.write(gsource)
|
|
281
|
+
all_outputs.append(oname)
|
|
282
|
+
_step("Files written", ", ".join(all_outputs))
|
|
283
|
+
|
|
284
|
+
else:
|
|
285
|
+
# ── Single-file pipeline ──────────────────────────────────
|
|
286
|
+
ext = os.path.splitext(file_path)[1]
|
|
287
|
+
|
|
288
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=ext, delete=False) as tmp:
|
|
289
|
+
tmp.write(source_code)
|
|
290
|
+
tmp_path = tmp.name
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
# Step 1: Parse and handle comments
|
|
294
|
+
parse_result = parser.parse(tmp_path)
|
|
295
|
+
parse_result.file_path = file_path
|
|
296
|
+
parse_result.source_code = source_code
|
|
297
|
+
_step("AST parsed",
|
|
298
|
+
f"{len(parse_result.symbols)} symbols, "
|
|
299
|
+
f"{len(parse_result.comments)} comments")
|
|
300
|
+
|
|
301
|
+
if keep_comments:
|
|
302
|
+
# Keep comments — will anonymize after renaming
|
|
303
|
+
clean_source = source_code
|
|
304
|
+
comment_count = len(parse_result.comments)
|
|
305
|
+
saved_comments = parse_result.comments
|
|
306
|
+
else:
|
|
307
|
+
stripper = CommentStripper()
|
|
308
|
+
clean_source, comment_count = stripper.strip(
|
|
309
|
+
source_code, parse_result.comments
|
|
310
|
+
)
|
|
311
|
+
saved_comments = None
|
|
312
|
+
_step("Comments stripped", f"{comment_count} removed")
|
|
313
|
+
|
|
314
|
+
# Step 2: Re-parse clean source for accurate offsets
|
|
315
|
+
with open(tmp_path, "w") as f:
|
|
316
|
+
f.write(clean_source)
|
|
317
|
+
|
|
318
|
+
clean_parse = parser.parse(tmp_path)
|
|
319
|
+
clean_parse.file_path = file_path
|
|
320
|
+
clean_parse.source_code = clean_source
|
|
321
|
+
|
|
322
|
+
# Step 3: Rename symbols
|
|
323
|
+
renamer = SymbolRenamer(ghost_map)
|
|
324
|
+
ghost_source = renamer.rename(clean_parse)
|
|
325
|
+
_step("Symbols renamed", f"{ghost_map.symbol_count} mapped")
|
|
326
|
+
|
|
327
|
+
# Step 4: Anonymize comments (if keeping them)
|
|
328
|
+
if keep_comments and saved_comments:
|
|
329
|
+
anonymizer = CommentAnonymizer(ghost_map)
|
|
330
|
+
# Re-parse the ghost source to find comment positions
|
|
331
|
+
with open(tmp_path, "w") as f:
|
|
332
|
+
f.write(ghost_source)
|
|
333
|
+
ghost_parse = parser.parse(tmp_path)
|
|
334
|
+
ghost_source, anon_count = anonymizer.anonymize(
|
|
335
|
+
ghost_source, ghost_parse.comments
|
|
336
|
+
)
|
|
337
|
+
_step("Comments anonymized", f"{anon_count} anonymized")
|
|
338
|
+
|
|
339
|
+
finally:
|
|
340
|
+
os.unlink(tmp_path)
|
|
341
|
+
|
|
342
|
+
# ── Level 2+: Literal Scrubbing ──────────────────────────────
|
|
343
|
+
scrub_result = None
|
|
344
|
+
if level >= 2:
|
|
345
|
+
scrubber = LiteralScrubber(ghost_map)
|
|
346
|
+
original_names = {
|
|
347
|
+
entry.original for entry in ghost_map._entries.values()
|
|
348
|
+
}
|
|
349
|
+
scrubber.set_known_symbols(original_names)
|
|
350
|
+
scrub_result = scrubber.scrub(ghost_source, file_path)
|
|
351
|
+
ghost_source = scrub_result.source
|
|
352
|
+
_step("Literals scrubbed",
|
|
353
|
+
f"{len(scrub_result.scrubbed)} scrubbed, "
|
|
354
|
+
f"{len(scrub_result.flagged)} flagged, "
|
|
355
|
+
f"{len(scrub_result.kept)} kept")
|
|
356
|
+
|
|
357
|
+
# ── Level 4: Dimension Generalization ─────────────────────────
|
|
358
|
+
dim_count = 0
|
|
359
|
+
if level >= 4:
|
|
360
|
+
ghost_source, dim_count = _generalize_dimensions(
|
|
361
|
+
ghost_source, ghost_map, file_path
|
|
362
|
+
)
|
|
363
|
+
_step("Dimensions generalized", f"{dim_count} loop bounds")
|
|
364
|
+
|
|
365
|
+
# ── Output ────────────────────────────────────────────────────
|
|
366
|
+
if output is None:
|
|
367
|
+
base = os.path.basename(file_path)
|
|
368
|
+
output = f"ghost_{base}"
|
|
369
|
+
|
|
370
|
+
if map_file is None:
|
|
371
|
+
ext = ".ghost" if use_encryption else ".json"
|
|
372
|
+
map_file = _generate_map_path(file_path).replace(".json", ext)
|
|
373
|
+
|
|
374
|
+
if not multi_mode:
|
|
375
|
+
with open(output, "w") as f:
|
|
376
|
+
f.write(ghost_source)
|
|
377
|
+
|
|
378
|
+
# Store paths for one-click reveal: overwrite original + auto-detect ghost file
|
|
379
|
+
ghost_map._metadata["original_file"] = os.path.abspath(file_path)
|
|
380
|
+
ghost_map._metadata["ghost_file"] = os.path.abspath(output)
|
|
381
|
+
ghost_map.save(map_file, passphrase=passphrase)
|
|
382
|
+
|
|
383
|
+
# Clipboard
|
|
384
|
+
copied = False
|
|
385
|
+
if clipboard:
|
|
386
|
+
copied = copy_to_clipboard(ghost_source)
|
|
387
|
+
if copied:
|
|
388
|
+
_step("Copied to clipboard", "ready to paste into AI")
|
|
389
|
+
else:
|
|
390
|
+
_warn("Clipboard unavailable — copy manually from output file")
|
|
391
|
+
|
|
392
|
+
# ── Audit Log ─────────────────────────────────────────────────
|
|
393
|
+
all_warnings = ghost_map.warnings
|
|
394
|
+
audit.log_hide(
|
|
395
|
+
source_files=list(file_paths),
|
|
396
|
+
scrub_level=level,
|
|
397
|
+
function_isolated=function if isolated else None,
|
|
398
|
+
symbols_scrubbed=ghost_map.symbol_count,
|
|
399
|
+
literals_scrubbed=len(scrub_result.scrubbed) if scrub_result else 0,
|
|
400
|
+
literals_flagged=len(scrub_result.flagged) if scrub_result else 0,
|
|
401
|
+
literals_kept=len(scrub_result.kept) if scrub_result else 0,
|
|
402
|
+
comments_stripped=comment_count,
|
|
403
|
+
warnings=all_warnings,
|
|
404
|
+
ghost_output_path=output,
|
|
405
|
+
map_path=map_file,
|
|
406
|
+
ghost_output_content=ghost_source,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# ── Risk Report ───────────────────────────────────────────────
|
|
410
|
+
if show_risk_report:
|
|
411
|
+
analyzer = RiskAnalyzer()
|
|
412
|
+
risk_report = analyzer.analyze(
|
|
413
|
+
ghost_map=ghost_map,
|
|
414
|
+
ghost_source=ghost_source,
|
|
415
|
+
level=level,
|
|
416
|
+
comment_count=comment_count,
|
|
417
|
+
keep_comments=keep_comments,
|
|
418
|
+
scrub_result=scrub_result,
|
|
419
|
+
function_isolated=function if isolated else None,
|
|
420
|
+
dim_count=dim_count,
|
|
421
|
+
file_count=len(file_paths),
|
|
422
|
+
)
|
|
423
|
+
click.echo(format_risk_report_cli(risk_report))
|
|
424
|
+
|
|
425
|
+
# Emit JSON for VS Code extension parsing
|
|
426
|
+
import json as _json
|
|
427
|
+
click.echo(f"RISK_REPORT_JSON: {_json.dumps(risk_report.to_dict())}")
|
|
428
|
+
|
|
429
|
+
# ── Summary ───────────────────────────────────────────────────
|
|
430
|
+
click.echo()
|
|
431
|
+
_divider()
|
|
432
|
+
click.secho(" HIDE COMPLETE", fg="green", bold=True)
|
|
433
|
+
_divider()
|
|
434
|
+
_info("Symbols renamed", str(ghost_map.symbol_count))
|
|
435
|
+
if keep_comments:
|
|
436
|
+
_info("Comments anonymized", str(comment_count))
|
|
437
|
+
else:
|
|
438
|
+
_info("Comments stripped", str(comment_count))
|
|
439
|
+
if isolated:
|
|
440
|
+
_info("Function isolated", function)
|
|
441
|
+
if scrub_result:
|
|
442
|
+
_info("Literals scrubbed", str(len(scrub_result.scrubbed)))
|
|
443
|
+
if scrub_result.flagged:
|
|
444
|
+
_info("Literals flagged", str(len(scrub_result.flagged)))
|
|
445
|
+
if level >= 4:
|
|
446
|
+
_info("Dimensions generalized", str(dim_count))
|
|
447
|
+
if all_warnings:
|
|
448
|
+
_info("Warnings", str(len(all_warnings)))
|
|
449
|
+
if use_encryption:
|
|
450
|
+
_info("Encryption", "enabled")
|
|
451
|
+
click.echo()
|
|
452
|
+
_info("Ghost output", output)
|
|
453
|
+
_info("Map file", map_file)
|
|
454
|
+
if copied:
|
|
455
|
+
click.secho("\n >> Ghost output is on your clipboard. Paste it into your AI. <<",
|
|
456
|
+
fg="green", bold=True)
|
|
457
|
+
click.echo()
|
|
458
|
+
|
|
459
|
+
# Show literal scrub details if any flagged
|
|
460
|
+
if scrub_result and scrub_result.flagged:
|
|
461
|
+
scrubber = LiteralScrubber(ghost_map)
|
|
462
|
+
click.echo(scrubber.summary(scrub_result))
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _generalize_dimensions(source: str, ghost_map: GhostMap,
|
|
466
|
+
file_path: str) -> tuple[str, int]:
|
|
467
|
+
"""Level 4: Replace loop bound literals with ghost constants."""
|
|
468
|
+
count = 0
|
|
469
|
+
pattern = re.compile(
|
|
470
|
+
r"(\bfor\s*\([^;]*;\s*\w+\s*[<>!=]+\s*)(\d+)(\s*;)"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def replacer(match):
|
|
474
|
+
nonlocal count
|
|
475
|
+
number = match.group(2)
|
|
476
|
+
num_val = int(number)
|
|
477
|
+
if num_val <= 1:
|
|
478
|
+
return match.group(0)
|
|
479
|
+
token = ghost_map.add_symbol(
|
|
480
|
+
original=number, kind="constant",
|
|
481
|
+
scope="loop_bound", source_file=file_path,
|
|
482
|
+
)
|
|
483
|
+
count += 1
|
|
484
|
+
return match.group(1) + token + match.group(3)
|
|
485
|
+
|
|
486
|
+
source = pattern.sub(replacer, source)
|
|
487
|
+
|
|
488
|
+
py_pattern = re.compile(r"(range\s*\()(\d+)(\s*\))")
|
|
489
|
+
|
|
490
|
+
def py_replacer(match):
|
|
491
|
+
nonlocal count
|
|
492
|
+
number = match.group(2)
|
|
493
|
+
num_val = int(number)
|
|
494
|
+
if num_val <= 1:
|
|
495
|
+
return match.group(0)
|
|
496
|
+
token = ghost_map.add_symbol(
|
|
497
|
+
original=number, kind="constant",
|
|
498
|
+
scope="loop_bound", source_file=file_path,
|
|
499
|
+
)
|
|
500
|
+
count += 1
|
|
501
|
+
return match.group(1) + token + match.group(3)
|
|
502
|
+
|
|
503
|
+
source = py_pattern.sub(py_replacer, source)
|
|
504
|
+
return source, count
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# ── REVEAL command ──────────────────────────────────────────────────
|
|
508
|
+
|
|
509
|
+
@main.command()
|
|
510
|
+
@click.argument("file_path", type=click.Path(exists=True))
|
|
511
|
+
@click.option("--map-file", "-m", type=click.Path(exists=True), required=True,
|
|
512
|
+
help="Path to the ghost map file")
|
|
513
|
+
@click.option("--output", "-o", type=click.Path(), default=None,
|
|
514
|
+
help="Output file path for code (default: revealed_<filename>)")
|
|
515
|
+
@click.option("--sent", "-s", type=click.Path(exists=True), default=None,
|
|
516
|
+
help="Original ghost file sent to AI (for diff analysis)")
|
|
517
|
+
@click.option("--mode", type=click.Choice(["code", "ai-response"]), default="code",
|
|
518
|
+
help="Mode: 'code' for pure code files, 'ai-response' for full AI responses")
|
|
519
|
+
def reveal(file_path: str, map_file: str, output: str | None,
|
|
520
|
+
sent: str | None, mode: str):
|
|
521
|
+
"""Restore original names from AI's response.
|
|
522
|
+
|
|
523
|
+
Two modes:
|
|
524
|
+
code — pure code file, simple token replacement
|
|
525
|
+
ai-response — full AI response with prose + code blocks
|
|
526
|
+
|
|
527
|
+
Examples:
|
|
528
|
+
ghost reveal fixed.cpp -m .ghostcode/maps/sample_20240101.json
|
|
529
|
+
ghost reveal ai_output.md -m map.json --mode ai-response --sent ghost_sample.cpp
|
|
530
|
+
"""
|
|
531
|
+
from .reveal.code_revealer import CodeRevealer
|
|
532
|
+
from .reveal.diff_analyzer import DiffAnalyzer
|
|
533
|
+
from .reveal.explanation_translator import ExplanationTranslator
|
|
534
|
+
|
|
535
|
+
_header("GHOST REVEAL")
|
|
536
|
+
_info("Mode", mode)
|
|
537
|
+
|
|
538
|
+
# Load map
|
|
539
|
+
ghost_map = GhostMap.load(map_file)
|
|
540
|
+
_step("Map loaded", f"{ghost_map.symbol_count} symbols from {map_file}")
|
|
541
|
+
|
|
542
|
+
# Read input
|
|
543
|
+
with open(file_path) as f:
|
|
544
|
+
content = f.read()
|
|
545
|
+
|
|
546
|
+
revealer = CodeRevealer(ghost_map)
|
|
547
|
+
|
|
548
|
+
if mode == "code":
|
|
549
|
+
# ── Simple code reveal ────────────────────────────────────
|
|
550
|
+
# Auto-detect the original ghost file from map metadata for annotation
|
|
551
|
+
sent_code = None
|
|
552
|
+
ghost_file_path = ghost_map._metadata.get("ghost_file", "")
|
|
553
|
+
if sent:
|
|
554
|
+
with open(sent) as f:
|
|
555
|
+
sent_code = f.read()
|
|
556
|
+
elif ghost_file_path and os.path.exists(ghost_file_path):
|
|
557
|
+
with open(ghost_file_path) as f:
|
|
558
|
+
sent_code = f.read()
|
|
559
|
+
_step("Auto-detected ghost file", os.path.basename(ghost_file_path))
|
|
560
|
+
|
|
561
|
+
# Diff analysis (run before reveal so we can pass result through)
|
|
562
|
+
diff_result = None
|
|
563
|
+
if sent_code:
|
|
564
|
+
analyzer = DiffAnalyzer()
|
|
565
|
+
diff_result = analyzer.analyze(sent_code, content)
|
|
566
|
+
|
|
567
|
+
restored, count, new_symbols = revealer.reveal_code(
|
|
568
|
+
content, original_ghost=sent_code, diff_result=diff_result
|
|
569
|
+
)
|
|
570
|
+
_step("Symbols restored", f"{count} tokens → original names")
|
|
571
|
+
|
|
572
|
+
if new_symbols:
|
|
573
|
+
_warn(f"{len(new_symbols)} new symbols detected (AI-introduced):")
|
|
574
|
+
for sym in new_symbols:
|
|
575
|
+
click.echo(f" {sym} → NEW_{sym}")
|
|
576
|
+
conf_color = {"HIGH": "green", "MEDIUM": "yellow", "LOW": "red"}.get(
|
|
577
|
+
diff_result.confidence.value, "white"
|
|
578
|
+
)
|
|
579
|
+
_step("Diff analyzed",
|
|
580
|
+
f"{len(diff_result.changes)} changes, confidence: "
|
|
581
|
+
+ click.style(diff_result.confidence.value, fg=conf_color))
|
|
582
|
+
|
|
583
|
+
# Output — overwrite original file by default
|
|
584
|
+
if output is None:
|
|
585
|
+
original_file = ghost_map._metadata.get("original_file", "")
|
|
586
|
+
if original_file and os.path.exists(os.path.dirname(original_file) or "."):
|
|
587
|
+
output = original_file
|
|
588
|
+
else:
|
|
589
|
+
base = os.path.basename(file_path)
|
|
590
|
+
output = f"revealed_{base}"
|
|
591
|
+
|
|
592
|
+
with open(output, "w") as f:
|
|
593
|
+
f.write(restored)
|
|
594
|
+
|
|
595
|
+
# Summary
|
|
596
|
+
click.echo()
|
|
597
|
+
_divider()
|
|
598
|
+
click.secho(" REVEAL COMPLETE", fg="green", bold=True)
|
|
599
|
+
_divider()
|
|
600
|
+
_info("Symbols restored", str(count))
|
|
601
|
+
_info("New symbols", f"{len(new_symbols)} (prefixed with NEW_)")
|
|
602
|
+
if diff_result:
|
|
603
|
+
_info("Confidence",
|
|
604
|
+
f"{diff_result.confidence.value} ({diff_result.confidence_score}/100)")
|
|
605
|
+
if diff_result.changes:
|
|
606
|
+
click.echo()
|
|
607
|
+
click.secho(" Changes detected:", bold=True)
|
|
608
|
+
for change in diff_result.changes:
|
|
609
|
+
icon = {
|
|
610
|
+
"modified": click.style("~", fg="yellow"),
|
|
611
|
+
"new_function": click.style("+", fg="green"),
|
|
612
|
+
"deleted_function": click.style("-", fg="red"),
|
|
613
|
+
"signature_change": click.style("!", fg="red"),
|
|
614
|
+
"new_variable": click.style("+", fg="green"),
|
|
615
|
+
"new_dependency": click.style("+", fg="cyan"),
|
|
616
|
+
}.get(change.type.value, "?")
|
|
617
|
+
click.echo(f" [{icon}] {change.detail}")
|
|
618
|
+
_info("Output", output)
|
|
619
|
+
click.echo()
|
|
620
|
+
|
|
621
|
+
else:
|
|
622
|
+
# ── AI response reveal (zone-aware) ───────────────────────
|
|
623
|
+
result = revealer.reveal_ai_response(content)
|
|
624
|
+
_step("Zones parsed", f"{result.symbols_restored} symbols restored")
|
|
625
|
+
|
|
626
|
+
# Explanation translator
|
|
627
|
+
stubs = [
|
|
628
|
+
token for token, entry in ghost_map._entries.items()
|
|
629
|
+
if "(stub)" in (entry.scope or "")
|
|
630
|
+
]
|
|
631
|
+
translator = ExplanationTranslator(ghost_map, stubs=stubs)
|
|
632
|
+
annotated_explanation, annotations = translator.annotate(
|
|
633
|
+
result.restored_explanation
|
|
634
|
+
)
|
|
635
|
+
_step("Explanation translated", f"{len(annotations)} annotations")
|
|
636
|
+
|
|
637
|
+
# Diff analysis
|
|
638
|
+
diff_result = None
|
|
639
|
+
if sent:
|
|
640
|
+
with open(sent) as f:
|
|
641
|
+
sent_code = f.read()
|
|
642
|
+
analyzer = DiffAnalyzer()
|
|
643
|
+
diff_result = analyzer.analyze(sent_code, result.restored_code)
|
|
644
|
+
_step("Diff analyzed", f"confidence: {diff_result.confidence.value}")
|
|
645
|
+
|
|
646
|
+
# Output files
|
|
647
|
+
if output is None:
|
|
648
|
+
base = os.path.splitext(os.path.basename(file_path))[0]
|
|
649
|
+
output = f"revealed_{base}"
|
|
650
|
+
|
|
651
|
+
code_output = output + os.path.splitext(file_path)[1]
|
|
652
|
+
explanation_output = output + "_explanation.md"
|
|
653
|
+
|
|
654
|
+
if result.restored_code:
|
|
655
|
+
with open(code_output, "w") as f:
|
|
656
|
+
f.write(result.restored_code)
|
|
657
|
+
|
|
658
|
+
with open(explanation_output, "w") as f:
|
|
659
|
+
f.write(annotated_explanation)
|
|
660
|
+
|
|
661
|
+
# Summary
|
|
662
|
+
click.echo()
|
|
663
|
+
_divider()
|
|
664
|
+
click.secho(" REVEAL COMPLETE", fg="green", bold=True)
|
|
665
|
+
_divider()
|
|
666
|
+
_info("Symbols restored", str(result.symbols_restored))
|
|
667
|
+
_info("New symbols", str(len(result.new_symbols)))
|
|
668
|
+
_info("Annotations", str(len(annotations)))
|
|
669
|
+
if diff_result:
|
|
670
|
+
_info("Confidence",
|
|
671
|
+
f"{diff_result.confidence.value} ({diff_result.confidence_score}/100)")
|
|
672
|
+
if annotations:
|
|
673
|
+
click.echo()
|
|
674
|
+
click.secho(" Annotations:", bold=True)
|
|
675
|
+
for ann in annotations:
|
|
676
|
+
if ann.type == "naming_advice":
|
|
677
|
+
icon = click.style("~~", fg="yellow")
|
|
678
|
+
else:
|
|
679
|
+
icon = click.style("!!", fg="red")
|
|
680
|
+
click.echo(f" [{icon}] {ann.note[:80]}")
|
|
681
|
+
click.echo()
|
|
682
|
+
if result.restored_code:
|
|
683
|
+
_info("Code output", code_output)
|
|
684
|
+
_info("Explanation", explanation_output)
|
|
685
|
+
click.echo()
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
# ── MAP command ─────────────────────────────────────────────────────
|
|
689
|
+
|
|
690
|
+
@main.command("map")
|
|
691
|
+
@click.argument("map_file", type=click.Path(exists=True))
|
|
692
|
+
def show_map(map_file: str):
|
|
693
|
+
"""Inspect a ghost map file."""
|
|
694
|
+
ghost_map = GhostMap.load(map_file)
|
|
695
|
+
|
|
696
|
+
_header(f"GHOST MAP — {os.path.basename(map_file)}")
|
|
697
|
+
_info("Total symbols", str(ghost_map.symbol_count))
|
|
698
|
+
_info("Files", ", ".join(ghost_map._metadata.get("files", [])))
|
|
699
|
+
click.echo()
|
|
700
|
+
|
|
701
|
+
forward = ghost_map.forward_map()
|
|
702
|
+
groups: dict[str, list[tuple[str, str]]] = {}
|
|
703
|
+
prefix_names = {
|
|
704
|
+
"gv": "Variables", "gf": "Functions", "gt": "Types",
|
|
705
|
+
"gc": "Constants", "gs": "Strings", "gm": "Macros",
|
|
706
|
+
"gn": "Namespaces",
|
|
707
|
+
}
|
|
708
|
+
prefix_colors = {
|
|
709
|
+
"gv": "white", "gf": "cyan", "gt": "magenta",
|
|
710
|
+
"gc": "yellow", "gs": "green", "gm": "red",
|
|
711
|
+
"gn": "blue",
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
for token, original in sorted(forward.items()):
|
|
715
|
+
prefix = token.split("_")[0]
|
|
716
|
+
groups.setdefault(prefix, []).append((token, original))
|
|
717
|
+
|
|
718
|
+
for prefix, items in groups.items():
|
|
719
|
+
label = prefix_names.get(prefix, prefix)
|
|
720
|
+
color = prefix_colors.get(prefix, "white")
|
|
721
|
+
click.secho(f" {label}:", fg=color, bold=True)
|
|
722
|
+
for token, original in items:
|
|
723
|
+
entry = ghost_map.get_entry(token)
|
|
724
|
+
scope = click.style(f" ({entry.scope})", dim=True) if entry and entry.scope else ""
|
|
725
|
+
click.echo(f" {click.style(token, fg=color)} → {original}{scope}")
|
|
726
|
+
click.echo()
|
|
727
|
+
|
|
728
|
+
if ghost_map.warnings:
|
|
729
|
+
click.secho(f" Warnings ({len(ghost_map.warnings)}):", fg="yellow", bold=True)
|
|
730
|
+
for w in ghost_map.warnings:
|
|
731
|
+
click.echo(f" {w['type']}: {w['symbol']} (line {w['line']})")
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
# ── STATUS command ──────────────────────────────────────────────────
|
|
735
|
+
|
|
736
|
+
@main.command()
|
|
737
|
+
def status():
|
|
738
|
+
"""Show GhostCode status and recent activity."""
|
|
739
|
+
config = load_config()
|
|
740
|
+
audit = AuditLogger()
|
|
741
|
+
|
|
742
|
+
_header("GHOSTCODE STATUS")
|
|
743
|
+
_info("Version", __version__)
|
|
744
|
+
|
|
745
|
+
# Config info
|
|
746
|
+
click.echo()
|
|
747
|
+
click.secho(" Configuration:", bold=True)
|
|
748
|
+
from .config import _find_repo_config
|
|
749
|
+
repo_config_path = _find_repo_config()
|
|
750
|
+
if repo_config_path:
|
|
751
|
+
_info(" Repo policy", repo_config_path)
|
|
752
|
+
else:
|
|
753
|
+
_info(" Repo policy", click.style("none", dim=True))
|
|
754
|
+
user_config = os.path.join(os.path.expanduser("~"), ".ghostcode", "config.yaml")
|
|
755
|
+
if os.path.exists(user_config):
|
|
756
|
+
_info(" User config", user_config)
|
|
757
|
+
else:
|
|
758
|
+
_info(" User config", click.style("none", dim=True))
|
|
759
|
+
_info(" Min level", str(config.min_scrub_level))
|
|
760
|
+
_info(" Default level", str(config.default_scrub_level))
|
|
761
|
+
_info(" Encrypt maps", str(config.encrypt_maps))
|
|
762
|
+
_info(" Audit enforced", str(config.enforce_audit))
|
|
763
|
+
if config.banned_patterns:
|
|
764
|
+
_info(" Banned patterns", ", ".join(config.banned_patterns))
|
|
765
|
+
|
|
766
|
+
# Active maps
|
|
767
|
+
map_dir = os.path.join(".ghostcode", "maps")
|
|
768
|
+
click.echo()
|
|
769
|
+
if os.path.exists(map_dir):
|
|
770
|
+
maps = [f for f in os.listdir(map_dir)
|
|
771
|
+
if f.endswith(".json") or f.endswith(".ghost")]
|
|
772
|
+
if maps:
|
|
773
|
+
click.secho(f" Active Maps ({len(maps)}):", bold=True)
|
|
774
|
+
for m in sorted(maps, reverse=True)[:5]:
|
|
775
|
+
mpath = os.path.join(map_dir, m)
|
|
776
|
+
size = os.path.getsize(mpath)
|
|
777
|
+
if m.endswith(".ghost"):
|
|
778
|
+
tag = click.style("encrypted", fg="green")
|
|
779
|
+
else:
|
|
780
|
+
tag = click.style("plaintext", fg="yellow")
|
|
781
|
+
click.echo(f" {m} ({size} bytes, {tag})")
|
|
782
|
+
else:
|
|
783
|
+
click.secho(" Active Maps: none", dim=True)
|
|
784
|
+
else:
|
|
785
|
+
click.secho(" Active Maps: none", dim=True)
|
|
786
|
+
|
|
787
|
+
# Recent audit
|
|
788
|
+
entries = audit.get_recent_entries(5)
|
|
789
|
+
click.echo()
|
|
790
|
+
if entries:
|
|
791
|
+
click.secho(f" Recent Activity ({len(entries)}):", bold=True)
|
|
792
|
+
for entry in reversed(entries):
|
|
793
|
+
ts = entry.get("timestamp", "?")[:19]
|
|
794
|
+
action = entry.get("action", "?")
|
|
795
|
+
if action == "hide":
|
|
796
|
+
files = entry.get("source_files", [])
|
|
797
|
+
lvl = entry.get("scrub_level", "?")
|
|
798
|
+
syms = entry.get("symbols_scrubbed", 0)
|
|
799
|
+
click.echo(
|
|
800
|
+
f" {click.style(ts, dim=True)} "
|
|
801
|
+
f"{click.style('HIDE', fg='cyan')} "
|
|
802
|
+
f"L{lvl} {syms} symbols {', '.join(files)}"
|
|
803
|
+
)
|
|
804
|
+
elif action == "reveal":
|
|
805
|
+
restored = entry.get("symbols_restored", 0)
|
|
806
|
+
conf = entry.get("confidence", "?")
|
|
807
|
+
click.echo(
|
|
808
|
+
f" {click.style(ts, dim=True)} "
|
|
809
|
+
f"{click.style('REVEAL', fg='green')} "
|
|
810
|
+
f"{restored} restored confidence={conf}"
|
|
811
|
+
)
|
|
812
|
+
else:
|
|
813
|
+
click.secho(" Recent Activity: none", dim=True)
|
|
814
|
+
click.echo()
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
# ── DEMO command ────────────────────────────────────────────────────
|
|
818
|
+
|
|
819
|
+
@main.command()
|
|
820
|
+
@click.option("--lang", type=click.Choice(["cpp", "python"]), default="python",
|
|
821
|
+
help="Demo language (default: python)")
|
|
822
|
+
def demo(lang: str):
|
|
823
|
+
"""Run an interactive GhostCode demo.
|
|
824
|
+
|
|
825
|
+
Shows the full hide → AI → reveal workflow with a real code sample.
|
|
826
|
+
Perfect for hackathon presentations and onboarding.
|
|
827
|
+
"""
|
|
828
|
+
import time
|
|
829
|
+
|
|
830
|
+
# Find fixture
|
|
831
|
+
fixture_dir = os.path.join(os.path.dirname(__file__), "..", "tests", "fixtures")
|
|
832
|
+
if not os.path.exists(fixture_dir):
|
|
833
|
+
fixture_dir = os.path.join(os.path.dirname(__file__), "fixtures")
|
|
834
|
+
fixture = os.path.join(fixture_dir, f"sample.{'py' if lang == 'python' else 'cpp'}")
|
|
835
|
+
|
|
836
|
+
if not os.path.exists(fixture):
|
|
837
|
+
_error(f"Demo fixture not found at {fixture}. Run from project root.")
|
|
838
|
+
|
|
839
|
+
click.echo(click.style(BANNER, fg="cyan", bold=True))
|
|
840
|
+
click.secho(" Privacy Proxy for Developers", fg="white", bold=True)
|
|
841
|
+
click.secho(" Your code stays anonymous. Your logic stays intact.\n", fg="white", dim=True)
|
|
842
|
+
|
|
843
|
+
# Read original
|
|
844
|
+
with open(fixture) as f:
|
|
845
|
+
original = f.read()
|
|
846
|
+
|
|
847
|
+
# ── Step 1: Show original ─────────────────────────────────────
|
|
848
|
+
click.secho(" STEP 1: Your original code (CONFIDENTIAL)", fg="red", bold=True)
|
|
849
|
+
_divider()
|
|
850
|
+
_show_code_preview(original, lang, max_lines=20)
|
|
851
|
+
click.echo()
|
|
852
|
+
_pause("Press Enter to see what GhostCode does...")
|
|
853
|
+
|
|
854
|
+
# ── Step 2: Run hide ──────────────────────────────────────────
|
|
855
|
+
click.secho("\n STEP 2: ghost hide (stripping business context)", fg="cyan", bold=True)
|
|
856
|
+
_divider()
|
|
857
|
+
|
|
858
|
+
parser = _get_parser(lang if lang == "python" else "cpp")
|
|
859
|
+
ext = ".py" if lang == "python" else ".cpp"
|
|
860
|
+
|
|
861
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=ext, delete=False) as tmp:
|
|
862
|
+
tmp.write(original)
|
|
863
|
+
tmp_path = tmp.name
|
|
864
|
+
|
|
865
|
+
try:
|
|
866
|
+
parse_result = parser.parse(tmp_path)
|
|
867
|
+
parse_result.source_code = original
|
|
868
|
+
parse_result.file_path = f"sample{ext}"
|
|
869
|
+
|
|
870
|
+
# Strip comments
|
|
871
|
+
stripper = CommentStripper()
|
|
872
|
+
clean_source, comment_count = stripper.strip(original, parse_result.comments)
|
|
873
|
+
_step("Comments stripped", f"{comment_count} removed")
|
|
874
|
+
time.sleep(0.3)
|
|
875
|
+
|
|
876
|
+
# Re-parse
|
|
877
|
+
with open(tmp_path, "w") as f:
|
|
878
|
+
f.write(clean_source)
|
|
879
|
+
clean_parse = parser.parse(tmp_path)
|
|
880
|
+
clean_parse.source_code = clean_source
|
|
881
|
+
clean_parse.file_path = f"sample{ext}"
|
|
882
|
+
|
|
883
|
+
# Rename
|
|
884
|
+
ghost_map = GhostMap()
|
|
885
|
+
renamer = SymbolRenamer(ghost_map)
|
|
886
|
+
ghost_source = renamer.rename(clean_parse)
|
|
887
|
+
_step("Symbols renamed", f"{ghost_map.symbol_count} → ghost tokens")
|
|
888
|
+
time.sleep(0.3)
|
|
889
|
+
|
|
890
|
+
# Literal scrub
|
|
891
|
+
scrubber = LiteralScrubber(ghost_map)
|
|
892
|
+
original_names = {e.original for e in ghost_map._entries.values()}
|
|
893
|
+
scrubber.set_known_symbols(original_names)
|
|
894
|
+
scrub_result = scrubber.scrub(ghost_source, f"sample{ext}")
|
|
895
|
+
ghost_source = scrub_result.source
|
|
896
|
+
_step("Literals scrubbed",
|
|
897
|
+
f"{len(scrub_result.scrubbed)} domain fingerprints removed")
|
|
898
|
+
time.sleep(0.3)
|
|
899
|
+
|
|
900
|
+
finally:
|
|
901
|
+
os.unlink(tmp_path)
|
|
902
|
+
|
|
903
|
+
click.echo()
|
|
904
|
+
click.secho(" Ghost output (safe to send to ANY AI):", fg="green", bold=True)
|
|
905
|
+
_divider()
|
|
906
|
+
_show_code_preview(ghost_source, lang, max_lines=20)
|
|
907
|
+
click.echo()
|
|
908
|
+
_pause("Press Enter to see the mapping...")
|
|
909
|
+
|
|
910
|
+
# ── Step 3: Show map ──────────────────────────────────────────
|
|
911
|
+
click.secho("\n STEP 3: Ghost Map (your private key)", fg="magenta", bold=True)
|
|
912
|
+
_divider()
|
|
913
|
+
forward = ghost_map.forward_map()
|
|
914
|
+
shown = 0
|
|
915
|
+
for token, orig in sorted(forward.items()):
|
|
916
|
+
if shown >= 10:
|
|
917
|
+
remaining = len(forward) - shown
|
|
918
|
+
click.secho(f" ... and {remaining} more", dim=True)
|
|
919
|
+
break
|
|
920
|
+
entry = ghost_map.get_entry(token)
|
|
921
|
+
kind = entry.kind if entry else "?"
|
|
922
|
+
click.echo(
|
|
923
|
+
f" {click.style(token, fg='cyan')} → "
|
|
924
|
+
f"{click.style(orig, fg='white', bold=True)} "
|
|
925
|
+
f"{click.style(f'({kind})', dim=True)}"
|
|
926
|
+
)
|
|
927
|
+
shown += 1
|
|
928
|
+
|
|
929
|
+
click.echo()
|
|
930
|
+
_pause("Press Enter to see the reveal...")
|
|
931
|
+
|
|
932
|
+
# ── Step 4: Simulate AI response and reveal ───────────────────
|
|
933
|
+
click.secho("\n STEP 4: AI responds → ghost reveal restores your names", fg="green", bold=True)
|
|
934
|
+
_divider()
|
|
935
|
+
|
|
936
|
+
# Simulate: reveal the ghost source back (as if AI returned it unchanged)
|
|
937
|
+
from .reveal.code_revealer import CodeRevealer
|
|
938
|
+
revealer = CodeRevealer(ghost_map)
|
|
939
|
+
restored, count, new_syms = revealer.reveal_code(ghost_source)
|
|
940
|
+
_step("Symbols restored", f"{count} ghost tokens → original names")
|
|
941
|
+
|
|
942
|
+
click.echo()
|
|
943
|
+
click.secho(" Restored code (back to YOUR names):", fg="green", bold=True)
|
|
944
|
+
_divider()
|
|
945
|
+
_show_code_preview(restored, lang, max_lines=15)
|
|
946
|
+
|
|
947
|
+
# ── Final pitch ───────────────────────────────────────────────
|
|
948
|
+
click.echo()
|
|
949
|
+
_divider()
|
|
950
|
+
click.secho(" GhostCode doesn't make your code invisible.", fg="white", bold=True)
|
|
951
|
+
click.secho(" It makes your code anonymous.", fg="cyan", bold=True)
|
|
952
|
+
click.echo()
|
|
953
|
+
click.secho(" Features:", bold=True)
|
|
954
|
+
click.echo(" • AST-based parsing (not regex) — understands your code structure")
|
|
955
|
+
click.echo(" • 4 privacy levels — from name+comment stripping to full isolation")
|
|
956
|
+
click.echo(" • Smart literal classification — scrubs domains, keeps math constants")
|
|
957
|
+
click.echo(" • Zone-aware reveal — handles AI prose, code blocks, inline code")
|
|
958
|
+
click.echo(" • Map encryption — AES-encrypted mapping files")
|
|
959
|
+
click.echo(" • Audit logging — compliance-ready with SHA-256 hashes")
|
|
960
|
+
click.echo(" • Clipboard integration — paste directly into ChatGPT/Claude")
|
|
961
|
+
click.echo()
|
|
962
|
+
click.secho(" ghost hide app.py && paste into AI && ghost reveal response.md", fg="cyan")
|
|
963
|
+
click.echo()
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _show_code_preview(code: str, lang: str, max_lines: int = 20):
|
|
967
|
+
"""Show a syntax-highlighted code preview."""
|
|
968
|
+
lines = code.split("\n")
|
|
969
|
+
for i, line in enumerate(lines[:max_lines]):
|
|
970
|
+
lineno = click.style(f" {i + 1:3d} │ ", dim=True)
|
|
971
|
+
click.echo(f" {lineno}{line}")
|
|
972
|
+
if len(lines) > max_lines:
|
|
973
|
+
click.secho(f" ... ({len(lines) - max_lines} more lines)", dim=True)
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def _pause(msg: str):
|
|
977
|
+
"""Pause for demo mode."""
|
|
978
|
+
click.secho(f" {msg}", fg="yellow", dim=True)
|
|
979
|
+
try:
|
|
980
|
+
input()
|
|
981
|
+
except (EOFError, KeyboardInterrupt):
|
|
982
|
+
click.echo()
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
if __name__ == "__main__":
|
|
986
|
+
main()
|