cloud-integrity-check 0.1.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.
- cloud_integrity_check/__init__.py +9 -0
- cloud_integrity_check/cli.py +36 -0
- cloud_integrity_check/core.py +61 -0
- cloud_integrity_check-0.1.0.dist-info/METADATA +68 -0
- cloud_integrity_check-0.1.0.dist-info/RECORD +8 -0
- cloud_integrity_check-0.1.0.dist-info/WHEEL +4 -0
- cloud_integrity_check-0.1.0.dist-info/entry_points.txt +2 -0
- cloud_integrity_check-0.1.0.dist-info/licenses/LICENSE +11 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""cloud-integrity-check — hash files into a manifest and verify integrity later.
|
|
2
|
+
|
|
3
|
+
Detects silent corruption, truncation or tampering after a cloud round-trip
|
|
4
|
+
(upload/download/sync). Pure standard library (hashlib) — no custom crypto.
|
|
5
|
+
"""
|
|
6
|
+
from .core import compute_manifest, verify, hash_file, Report
|
|
7
|
+
|
|
8
|
+
__all__ = ["compute_manifest", "verify", "hash_file", "Report"]
|
|
9
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import argparse, json, sys
|
|
3
|
+
from .core import compute_manifest, verify
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main(argv=None) -> int:
|
|
7
|
+
p = argparse.ArgumentParser(prog="cloud-integrity-check",
|
|
8
|
+
description="Hash files into a manifest and verify integrity after a cloud round-trip.")
|
|
9
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
10
|
+
c = sub.add_parser("create", help="create a manifest of a file or directory")
|
|
11
|
+
c.add_argument("path"); c.add_argument("-o", "--out", default="integrity.json")
|
|
12
|
+
c.add_argument("--algo", default="sha256")
|
|
13
|
+
v = sub.add_parser("verify", help="verify a path against a manifest")
|
|
14
|
+
v.add_argument("manifest"); v.add_argument("path"); v.add_argument("--algo", default="sha256")
|
|
15
|
+
a = p.parse_args(argv)
|
|
16
|
+
|
|
17
|
+
if a.cmd == "create":
|
|
18
|
+
m = compute_manifest(a.path, a.algo)
|
|
19
|
+
with open(a.out, "w", encoding="utf-8") as f:
|
|
20
|
+
json.dump({"algo": a.algo, "files": m}, f, indent=2)
|
|
21
|
+
print(f"Wrote {a.out} ({len(m)} file(s), {a.algo}).")
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
with open(a.manifest, encoding="utf-8") as f:
|
|
25
|
+
data = json.load(f)
|
|
26
|
+
m = data.get("files", data)
|
|
27
|
+
rep = verify(m, a.path, data.get("algo", a.algo))
|
|
28
|
+
print(f"OK: {len(rep.ok)} Changed: {len(rep.changed)} Missing: {len(rep.missing)} Added: {len(rep.added)}")
|
|
29
|
+
for label, items in (("CHANGED", rep.changed), ("MISSING", rep.missing), ("ADDED", rep.added)):
|
|
30
|
+
for it in items:
|
|
31
|
+
print(f" {label}: {it}")
|
|
32
|
+
return 0 if rep.intact else 2
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import hashlib
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
|
|
7
|
+
CHUNK = 1024 * 1024 # 1 MiB
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def hash_file(path: str, algo: str = "sha256") -> str:
|
|
11
|
+
"""Return the hex digest of a file, read in chunks (memory-safe for big files)."""
|
|
12
|
+
h = hashlib.new(algo)
|
|
13
|
+
with open(path, "rb") as f:
|
|
14
|
+
for block in iter(lambda: f.read(CHUNK), b""):
|
|
15
|
+
h.update(block)
|
|
16
|
+
return h.hexdigest()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def compute_manifest(root: str, algo: str = "sha256") -> Dict[str, str]:
|
|
20
|
+
"""Map every file under root to its hash, keyed by POSIX-style relative path.
|
|
21
|
+
|
|
22
|
+
A single file yields a one-entry manifest keyed by its basename.
|
|
23
|
+
"""
|
|
24
|
+
if os.path.isfile(root):
|
|
25
|
+
return {os.path.basename(root): hash_file(root, algo)}
|
|
26
|
+
manifest: Dict[str, str] = {}
|
|
27
|
+
for dirpath, _dirs, files in os.walk(root):
|
|
28
|
+
for name in files:
|
|
29
|
+
full = os.path.join(dirpath, name)
|
|
30
|
+
rel = os.path.relpath(full, root).replace(os.sep, "/")
|
|
31
|
+
manifest[rel] = hash_file(full, algo)
|
|
32
|
+
return manifest
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Report:
|
|
37
|
+
ok: List[str] = field(default_factory=list) # unchanged
|
|
38
|
+
changed: List[str] = field(default_factory=list) # hash differs
|
|
39
|
+
missing: List[str] = field(default_factory=list) # in manifest, not on disk
|
|
40
|
+
added: List[str] = field(default_factory=list) # on disk, not in manifest
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def intact(self) -> bool:
|
|
44
|
+
return not (self.changed or self.missing)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def verify(manifest: Dict[str, str], root: str, algo: str = "sha256") -> Report:
|
|
48
|
+
"""Compare a stored manifest against the current state of root."""
|
|
49
|
+
current = compute_manifest(root, algo)
|
|
50
|
+
rep = Report()
|
|
51
|
+
for path, digest in manifest.items():
|
|
52
|
+
if path not in current:
|
|
53
|
+
rep.missing.append(path)
|
|
54
|
+
elif current[path] != digest:
|
|
55
|
+
rep.changed.append(path)
|
|
56
|
+
else:
|
|
57
|
+
rep.ok.append(path)
|
|
58
|
+
for path in current:
|
|
59
|
+
if path not in manifest:
|
|
60
|
+
rep.added.append(path)
|
|
61
|
+
return rep
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cloud-integrity-check
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hash files into a manifest and verify their integrity after a cloud upload/download/sync — detect silent corruption, truncation or tampering. Standard-library only.
|
|
5
|
+
Project-URL: Homepage, https://priviy.com
|
|
6
|
+
Project-URL: Documentation, https://priviy.com
|
|
7
|
+
Author: Eric Gerard
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: backup,checksum,cloud,integrity,manifest,sha256,tamper,verification
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# cloud-integrity-check
|
|
24
|
+
|
|
25
|
+
Hash your files into a manifest, then **verify they are byte-for-byte intact**
|
|
26
|
+
after a cloud upload, download or sync. Catches silent corruption, truncation
|
|
27
|
+
or tampering that you would otherwise not notice. **Standard library only**
|
|
28
|
+
(SHA-256 via `hashlib`) — no dependencies, no custom crypto.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
pip install cloud-integrity-check
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Use
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
# Before uploading: snapshot the integrity of a file or folder
|
|
40
|
+
cloud-integrity-check create ~/Documents -o documents.json
|
|
41
|
+
|
|
42
|
+
# After downloading from your cloud: verify nothing changed
|
|
43
|
+
cloud-integrity-check verify documents.json ~/Documents
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Output reports per-file `OK / CHANGED / MISSING / ADDED`, and exits non-zero if
|
|
47
|
+
anything changed or went missing — handy in scripts and CI.
|
|
48
|
+
|
|
49
|
+
## As a library
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from cloud_integrity_check import compute_manifest, verify
|
|
53
|
+
|
|
54
|
+
m = compute_manifest("/data")
|
|
55
|
+
report = verify(m, "/data")
|
|
56
|
+
print(report.intact, report.changed, report.missing)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Why
|
|
60
|
+
|
|
61
|
+
End-to-end encrypted clouds protect *confidentiality*; this verifies
|
|
62
|
+
*integrity* — that what you get back is exactly what you stored. A practical
|
|
63
|
+
companion to a zero-knowledge backup. More on private, verifiable cloud storage:
|
|
64
|
+
**[priviy.com](https://priviy.com)**.
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
cloud_integrity_check/__init__.py,sha256=P7GTEG9y8hwUUYy8Zfvc4ub-QnTRS1Iz82eNHM5nLRA,392
|
|
2
|
+
cloud_integrity_check/cli.py,sha256=hT6X77PfXIiFaECx_KjXM-NHnLFEkrcpvaGYG1MUjHo,1566
|
|
3
|
+
cloud_integrity_check/core.py,sha256=FthM1BY20QYsnZfoKEkKD-UBeUDiL9EJispCNXPwd_A,2073
|
|
4
|
+
cloud_integrity_check-0.1.0.dist-info/METADATA,sha256=Fjx9i7OTHpRZSXb-K3XbCz6knDgrxm9lHzf4OBU2lgY,2193
|
|
5
|
+
cloud_integrity_check-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
6
|
+
cloud_integrity_check-0.1.0.dist-info/entry_points.txt,sha256=Id1SVBgQYFxf5y5f653l0l2otyE4Ju-MA0JqTVcU5cQ,73
|
|
7
|
+
cloud_integrity_check-0.1.0.dist-info/licenses/LICENSE,sha256=8lsqmsA61fjwx9wiQPaJIcsgHocgr0Lih-on1Kfz0xI,486
|
|
8
|
+
cloud_integrity_check-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eric Gerard
|
|
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 the rights to use, copy, modify,
|
|
8
|
+
merge, publish, distribute, sublicense, and/or sell copies, subject to the
|
|
9
|
+
above copyright notice and this permission notice being included.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|