PyDA Course
متوسط نصف موجّه ⏱ 90 دقيقة ⚡ +100 XP +15 نقطة لكل خطوة

سجل التدقيق

SecurityBackendData Visualization

سجل التدقيق هو السجل الذي تعرضه على المحقّق بعد أن يحدث الخطأ: مَن فعل ماذا، وبأي ترتيب، والأهم ، هل عُدّل أيٌّ منه بهدوء من بعده. ملف نصي من أسطر لا يثبت شيئًا بنفسه؛ فتحرير نص عادي يبدو مطابقًا لحدث حقيقي. يبني هذا المشروع البنية التي تجعل إعادة الكتابة قابلة للاكتشاف: سجل يُكتب بالإلحاق فقط حيث يحمل كل إدخال تجزئة SHA-256 لمضمونه الخاص إضافةً إلى تجزئة الإدخال السابق، مكوِّنًا سلسلة. غيِّر سطرًا واحدًا في أي مكان فتنكسر كل وصلة لاحقة؛ وتمريرة verify() واحدة تبلغ عن الإدخال الملموس بالضبط. حول هذا القلب تضيف استعلامات بالخطورة والمصدر، وتقليم احتفاظ يُبقي السلسلة سليمة، وتصدير JSONL لأدوات الامتثال ولوحات المعلومات. يعمل كل شيء على المكتبة القياسية وهو حتمي ، نفس الأحداث تتحقق بالطريقة ذاتها في كل مرة.

هذا يفترض الصفوف والأساليب، والمدخلات/المخرجات من الملفات، ونظرة أولى على hashlib.sha256. إنه مشروع اختياري وغير مُقيَّم ، راجع مشاريع من العالم الحقيقي للاطلاع على القائمة الكاملة والنامية.

🎯 ما ستفعله

  1. كتابة سجل إضافة-فقط يخزّن الأحداث واحدًا في كل سطر.
  2. إضافة سلسلة تجزئة، ثم إثبات أنها تلتقط إدخالًا مُزوَّرًا.
  3. الاستعلام بالخطورة والمصدر، وعدّ الأحداث لكل خطورة.
  4. تقليم الإدخالات القديمة بالاحتفاظ مع إبقاء التحقق أخضر.
  5. تصدير موجز امتثال JSONL وملخصًا قابلًا للقراءة البشرية.

أين تُشغّل هذا

محليًا باستخدام uv هو المسار الموصى به ، السجل مكتبة قياسية صرفة (hashlib وpathlib وjson)، فكل ما تحتاجه uv init واحد.

Google Colab وKaggle Notebooks وBinder يشغّلون كل خطوة دون تعديل. استخدم مسارًا محليًا للمشروع (مثل audit.log) لا مسار نظام؛ فالدفاتر وBinder يسمحان لهذا الملف بالعيش بجوار الكود.

Open In Colab Open In Kaggle Binder

الإعداد

كل ما تحتاجه قبل أول حدث.

أنشئ المشروع

uv init audit-logger
cd audit-logger

لا تبعيات. السجل ملف .txt بحدث واحد لكل سطر؛ ومنطق «الإلحاق فقط» هو open(..., "a") فقط.

✅ قائمة التحقق

  • ✅ يُنشئ uv init audit-logger المشروع وملف main.py.
  • ✅ ينجح uv run python3 -c "import hashlib, pathlib, json" ، كلها مكتبة قياسية.

🤔 سؤال (أسئلة) سقراطي(ة) :

  • سطر سجل مثل INFO auth login ok وحده لا يثبت شيئًا عن صحته الذاتية. بأي خاصيتين يجب أن يتمتّع سجل مقاوم للتلاعب أبعد من «إنه ملف كتبه أحدهم»؟
  • تسلسل السلسلة كل إدخال إلى سَلَفه، فالـترتيب جزء من الدليل. لماذا يهم الترتيب لسجل تدقيق ، ماذا كان سيخفي سجلٌ مزوَّر لكن مُعاد ترتيبه؟

الخطوة 1: سجل أحداث للإلحاق فقط

أولًا، تسجيل إلحاق-فقط نزيه عادي: تتحول الأحداث إلى أسطر في ملف. تأتي مقاومة التلاعب في الخطوة 2.

1.1 دالة الملخّص

👟 تلميح البداية : اكتب digest(*parts) التي تضمّ الأجزاء بـ| وتعيد ملخص SHA-256 سداسيًا ، هو الغراء لكل تجزئة ستحسبها.

# main.py
import hashlib, pathlib, json

def digest(*parts):
    return hashlib.sha256("|".join(parts).encode()).hexdigest()

print(digest("1", "2025-06-01T10:00:00", "INFO", "auth", "login ok"))

يجعل "|".join(parts) السلسلة التي تحصّنها خالية من الالتباس: دون فاصل، يتصادم "a" + "bc" مع "ab" + "c"؛ مع |، يختلف ("a","bc") و("ab","c") في البايتات. الملخص السداسي حتمي ، نفس المدخلات، نفس الناتج، للأبد ، وهي الخاصية التي تستند إليها السلسلة كلها.

🎯 الناتج المتوقع : سلسلة سداسية من 64 محرفًا (مثل f0c2…): ملخصات SHA-256 دائمًا 64 محرفًا سداسيًا مهما كان طول المدخلات.

🩹 إذا لم يعمل : إن اختلف طول الناتج عن 64، فأنت لا تستدعي sha256 (md5 يعطي 32). إذا ظهر TypeError، فقد تسلّل جزء غير نصي ، حوِّله بـstr() أو رمِّزه أولًا.

1.2 ألحِق الأحداث كأسطر

👟 تلميح البداية : اكتب AuditLog(path) مع append(severity, source, message, ts) تُلحِق سطرًا واحدًا لكل حدث مضمومًا بـ|.

# main.py (continued)
class AuditLog:
    def __init__(self, path):
        self.path = pathlib.Path(path)
        self._seq = 0

    def append(self, severity, source, message, ts="2025-06-01T10:00:00"):
        self._seq += 1
        payload = [str(self._seq), ts, severity, source, message]
        with self.path.open("a") as f:
            f.write("|".join(payload) + "\n")
        return self._seq

log = AuditLog("audit.log")
log.append("INFO", "auth", "login ok", ts="2025-06-01T10:00:00")
log.append("INFO", "auth", "logout ok", ts="2025-06-01T10:01:00")
print(log.path.read_text())

open("a") هو النمط الذي يجعل وعد الإلحاق-فقط حقيقيًا: كل استدعاء يكتب في النهاية ولا يعيد أبدًا كتابة بايتات سابقة. يمنح عداد seq الأحداث ترتيبًا صريحًا ينجو حتى لو تساوت الطوابع الزمنية. الحمولة المضمومة بـ| هي سجل بيانات السجل ، تشتبك حولها السلسلة في الخطوة 2.

🎯 الناتج المتوقع:

1|2025-06-01T10:00:00|INFO|auth|login ok
2|2025-06-01T10:01:00|INFO|auth|logout ok

🩹 إذا لم يعمل : إن استبدل الملف نفسه بدل أن يُلحِق، فقد استخدم open النمط "w". إذا تلاصقت الأسطر، فإن \n الختامي ناقص من الكتابة.

1.3 احصل على مُرسأة

👟 تلميح البداية : أضف GENESIS = digest("GENESIS") كي يكون لأول إدخال تجزئة يشير إليها.

# main.py (continued)
GENESIS = digest("GENESIS")
print(GENESIS[:16], "...")

كل سلسلة تحتاج سَلَف أول وصلة فيها. GENESIS هي تلك المرسأة الثابتة: الإدخال 1 يشير إليها، وبمجرد وجود الإدخال 1 تشير السلسلة إلى إدخالات حقيقية فقط. لا سرّ في السلسلة "GENESIS" ، دورها أن تكون نقطة بداية ثابتة معلومة يتحقق الجميع مقابلها.

🎯 الناتج المتوقع : ستة عشر محرفًا سداسيًا يليها ... (الـ64 الكاملة في سطر منطقة 1.1 ، نفس الدالة المساعدة، نفس الدالة).

🩹 إذا لم يعمل : إذا اختلفت GENESIS بين الجريتين، فأنت تُحصّن جزءًا معتمدًا على الزمن. يجب أن تكون حرفية.

1.4 تحقّق من طبقة الإلحاق

✅ قائمة التحقق

  • ✅ استدعاءا append ينتجان سطرين مضمومين بـ| بالضبط، بالترتيب.
  • ✅ إعادة فتح نفس مسار AuditLog والإلحاق يكتب السطر الثالث في النهاية.
  • GENESIS ثابتة ، نفس القيمة في كل جري تفسير.

🤔 سؤال (أسئلة) سقراطي(ة) :

  • الإلحاق فقط هنا سياسة (أنت من يتحكم في الكود الذي يكتبها). أين يجب أن يعيش البرهان الحقيقي على أن «لا أحد أعاد كتابة التاريخ» ، في عرف الكتابة، أم في شيء قابل للفحص لاحقًا؟ ذلك الشيء القابل للفحص هو الخطوة 2.
  • يحمل الملف الأحداث نصًا صريحًا، يقرؤه أي شخص. هل هذا ضعف لسجل تدقيق، وماذا تضيف ، تشفيرًا أم توقيعات أم أذونات ، دون كسر السلسلة؟

الخطوة 2: سلسلة التجزئة ، واختبار التلاعب

الآن المكسب: يخزّن كل إدخال تجزئة الإدخال السابق، ما يجعل أي تعديل يكسر السلسلة. ثم تتحقق ، وتشاهده يلتقط تعديلًا مُدخَلًا.

2.1 اربط كل إدخال بسَلَفه

👟 تلميح البداية : داخل append، اقرأ آخر تجزئة مخزنة (بدءًا من GENESIS)، واحسب الوصلة التالية كـdigest(*payload, prev)، وخزّن prev والتجزئة الجديدة في السطر.

# main.py (continued)
    def rows(self):
        return [line.split("|") for line in self.path.read_text().splitlines()]

    def _last_hash(self):
        if not self.path.exists() or not self.path.read_text().strip():
            return GENESIS
        return self.rows()[-1][-1]

    def append(self, severity, source, message, ts="2025-06-01T10:00:00"):
        self._seq += 1
        prev = self._last_hash()
        payload = [str(self._seq), ts, severity, source, message]
        h = digest(*payload, prev)
        with self.path.open("a") as f:
            f.write("|".join(payload + [prev, h]) + "\n")
        return self._seq, h

log.append("WARN", "payments", "retry #1", ts="2025-06-01T10:02:00")
row = log.rows()[-1]
print(row)

كل سطر الآن سبعة حقول: حقول البيانات الخمسة، والتجزئة السابقة، وتجزئة الإدخال نفسه digest(*payload, prev). التجزئة تشمل prev، فالترتيب جزء من البرهان. يقرأ الإدخال التالي آخر تجزئة لهذا الإدخال ويلفّها معه ، سلسلة حرفية، وصلة لكل سطر.

🎯 الناتج المتوقع : قائمة من 7 حقول آخرها تجزئة من 64 محرفًا، مثل ['3', '2025-06-01T10:02:00', 'WARN', 'payments', 'retry #1', '…', '…'].

🩹 إذا لم يعمل : إذا كان السطر ستة حقول، فعملية payload + [prev, h] لم تُضم. إذا لم تتغير التجزئة المخزنة بين الإدخالات، فـ_last_hash لا يقرأ السطر السابق.

2.2 تحقّق من السلسلة

👟 تلميح البداية : اكتب verify() التي تمشي الصفوف، معيدة حساب كل تجزئة متوقعة من الحمولة وprev، ومعيدة (ok, position) حيث الكسر.

# main.py (continued)
    def verify(self):
        expected = GENESIS
        for i, row in enumerate(self.rows()):
            payload, prev, stored = row[:5], row[5], row[6]
            if prev != expected:
                return False, i
            expected = digest(*payload, prev)
            if stored != expected:
                return False, i
        n = len(self.rows())
        return (True, n) if n else (False, 0)

log.append("ERROR", "payments", "charge declined", ts="2025-06-01T10:03:00")
log.append("ERROR", "net", "timeout", ts="2025-06-01T10:04:00")
log.append("WARN", "payments", "charge recovered", ts="2025-06-01T10:05:00")
print("verify:", log.verify())

يعيد verify بثّ الدالة ذاتها التي استخدمها append: يبدأ من GENESIS، وعند كل صف يؤكد أن prev المخزن يطابق حيث وُجدت المشية، ثم يؤكد أن التجزئة المخزنة تساوي التجزئة التي كان append سيكتبها. سلسلة لم تُمس تمشي كل الصفوف الستة وصولًا إلى (True, 6).

🎯 الناتج المتوقع : verify: (True, 6).

🩹 إذا لم يعمل : إذا طبع (True, 6) كـ(False, 0)، فشريحة الحمولة في verify أسقطت حقل الرسالة ، كثير من المبتدئين يستخدمون row[:4] فيكسّرون كل تجزئة. استخدم row[:5] (كل حقول البيانات الخمسة).

2.3 ازرع تلاعبًا والتقطه

👟 تلميح البداية : أفسد رسالة الصف 4 ثم تحقق مجددًا ، يجب أن يشير الكسر إلى ذلك الإدخال بالضبط.

# main.py (continued)
lines = open("audit.log").readlines()                 # read all lines first
fields = lines[3].rstrip("\n").split("|")
fields[4] = fields[4].replace("declined", "DECLINED")
lines[3] = "|".join(fields) + "\n"
open("audit.log", "w").write("".join(lines))          # truncate only at the end

print("after edit:", log.verify())

إعادة كتابة الملف ليس شيئًا مميزًا ، النقطة أن الأداة تلاحظ. تغيّرت حمولة الصف 4، فتوقفت تجزئته المخزنة عن مطابقة digest(*payload, prev)، ويُبلغ verify عن الكسر عند فهرس الصف 3. أي تعديل في أي مكان يُلتقط، لأن كل وصلة سلسلة لاحقة ستخالف أيضًا. (استعد الملف ، أعد كتابته من الصفر ، قبل الخطوة 3.)

🎯 الناتج المتوقع : after edit: (False, 3) ، الإدخال المزوَّر هو الإدخال 4 (الفهرس 3).

🩹 إذا لم يعمل : إذا أبلغ التحقق عن فهرس لاحق، فالتعديل غيّر بايتات تغذّي تجزئة مخزنة لاحقة فقط ، تحقق أنك غيّرت حقل الرسالة (الفهرس 4) لا حقل التجزئة (الفهرس 6).

2.4 تحقّق من السلسلة

✅ قائمة التحقق

  • ✅ ستة إدخالات نزيهة تتحقق كـ(True, 6).
  • ✅ تعديل رسالة الإدخال 4 يعطي (False, 3).
  • ✅ تعديل أي إدخال ، رسالة أو خطورة أو ترتيبًا ، يكسر في ذلك الإدخال أو بعده.

🤔 سؤال (أسئلة) سقراطي(ة) :

  • تلتقط السلسلة التعديلات لكن ليس حذف الملف كله ولا استعادة جملية. ما الذي يميّز مقاومة التلاعب (هذه الخطوة) عن التوقيعات الرقمية (مفتاحك الخاص)، وأي قلق تحلّه كل منهما؟
  • يعيد verify الحساب من GENESIS في كل مرة. لو كان السجل مليون إدخال، أين تذهب التكلفة ، وما الإضافة الرخيصة (خزّن آخر تجزئة، وأعد التحقق من هناك) التي تُبقي الفحوص الموضعية سريعة؟

الخطوة 3: الاستعلام والعدّاد

كومة أسطر مقاومة للتلاعب ما زالت بحاجة إلى أن تُسأل أسئلة. تضيف الخطوة 3 مرشحات وعدّادات.

3.1 صفِّ بالخطورة والمصدر

👟 تلميح البداية : اكتب select(severity=None, source=None) معيدة الصفوف المطابقة كحقول البيانات الأربعة التي يقرؤها الناس.

# main.py (continued)
    def select(self, *, severity=None, source=None):
        out = []
        for row in self.rows():
            if severity and row[2] != severity:
                continue
            if source and row[3] != source:
                continue
            out.append(row[:4])
        return out

# fresh, intact log with the full six-event feed
fresh = AuditLog("audit2.log")
for s, src, msg, ts in [
    ("INFO", "auth", "login ok", "2025-06-01T10:00:00"),
    ("INFO", "auth", "logout ok", "2025-06-01T10:01:00"),
    ("WARN", "payments", "retry #1", "2025-06-01T10:02:00"),
    ("ERROR", "payments", "charge declined", "2025-06-01T10:03:00"),
    ("ERROR", "net", "timeout", "2025-06-01T10:04:00"),
    ("WARN", "payments", "charge recovered", "2025-06-01T10:05:00"),
]:
    fresh.append(s, src, msg, ts=ts)

print([r[2:4] for r in fresh.select(severity="ERROR")])
print([r[:2] for r in fresh.select(source="payments")])

select مرشّح صافٍ فوق rows(): لا حالة، لا تحوير ، نفس الصفوف داخلة، نفس الأجوبة خارجة، حتميًا. إمساك حقول البيانات row[:4] (وإسقاط التجزئتين) يجعل قائمة النتائج قابلة للقراءة ويُبقي التجزئات مرئية في rows() حين تحتاج التحقق.

🎯 الناتج المتوقع:

[['ERROR', 'payments'], ['ERROR', 'net']]
[['1', '2025-06-01T10:00:00'], ['3', '2025-06-01T10:02:00'], ['4', '2025-06-01T10:03:00'], ['6', '2025-06-01T10:05:00']]

🩹 إذا لم يعمل : إذا أعاد المرشّح لا شيء، فحالة الحرف للمصدر/الخطورة تختلف عن السجل (المخزّن ERROR مقابل المستعلَم error). إذا أعاد المرشحان السجل كله، فقد استُبدلت قفزتان continue بإلحاقات، أو لم تصل وسيطات الكلمات المفتاحية إلى الأسلوب.

3.2 عدّادات للوحة المعلومات

👟 تلميح البداية : استخدم Counter على حقل الخطورة لتحصل على الإجماليات لكل خطورة في سطر واحد.

# main.py (continued)
from collections import Counter

def counts(rows):
    return dict(Counter(r[2] for r in rows))

print(counts(fresh.rows()))

جرّد Counter(r[2] for r in rows) كل سطر سجل حسب الخطورة وأعاد العدّادات: هذه هي الأرقام التي يعرضها ودجيتك «أخطاء آخر 24 ساعة». ولأنه يعمل فوق rows() (التي ما زالت تحمل السلسلة)، تغذّي البيانات ذاتها اللوحة والتحقق معًا.

🎯 الناتج المتوقع : {'INFO': 2, 'WARN': 2, 'ERROR': 2}.

🩹 إذا لم يعمل : إذا غابت خطورة من القاموس، فـCounter يضع مفتاحًا لما عَدّه فقط ، خطورة بلا أحداث لن تظهر. إذا تجاوز مجموع العدّادات ستة، فالملف يحمل أسطرًا مكررة متبقية من عرض تلاعب الخطوة 2 ، ابدأ من جديد بـaudit2.log.

3.3 تحقّق من طبقة الاستعلام

✅ قائمة التحقق

  • ✅ يعيد select(severity="ERROR") الإدخالين 4 و5 بالضبط.
  • ✅ يعيد select(source="payments") خمسة إدخالات: الأرقام التسلسلية 1 و3 و4 و5 و6.
  • ✅ يعيد counts(rows) القيمة {'INFO': 2, 'WARN': 2, 'ERROR': 2} على السجل سليم الوصلات.

🤔 سؤال (أسئلة) سقراطي(ة) :

  • يعيد select نسخًا (row[:4])، لا مقابض إلى صفوف داخلية. لو حوّر مستدعٍ ما عرضًا قائمةً معادًة (غيّر خطورة)، فهل يتغير الملف أيضًا ، وهل تلك خاصية تريدها لسجل تدقيق؟
  • تعرض لوحة «ERROR: 2». والملف نفسه في نسخة الخطوة 2 المزوّرة يحسب أرقامًا مختلفة. ما الذي يربحه «تحقّق من السجل قبل أن تثق في أرقام اللوحة» ولا تستطيع اللوحة وحدها منحه؟

الخطوة 4: الاحتفاظ ، التقليم دون كسر السلسلة

تنمو السجلات إلى الأبد؛ سياسات الاحتفاظ تكبّلها. تقلّم الخطوة 4 الإدخالات القديمة وتعيد تثبيت السلسلة الباقية كي يظل السجل المقلَّم يتحقق.

4.1 قصّ الإدخالات القديمة

👟 تلميح البداية : اكتب retain(since_seq) التي تُبقي الصفوف ذات seq >= since_seq وتعيد كتابة الملف.

# main.py (continued)
    def retain(self, since_seq):
        kept = [r for r in self.rows() if int(r[0]) >= since_seq]
        with self.path.open("w") as f:
            expected = GENESIS
            for row in kept:
                payload = row[:5]
                prev = row[5]
                if prev != expected:
                    prev = expected
                expected = digest(*payload, prev)
                f.write("|".join(payload + [prev, expected]) + "\n")
        return len(kept)

print("kept:", fresh.retain(3))
print(fresh.path.read_text())

إسقاط صفوف كانت تحمل وصَلات السلسلة القديمة سيترك قيم prev للناجين يتيمة. يصلح retain ذلك بإعادة بدء المشية عند GENESIS وإعادة حساب prev/التجزئة لكل ناجٍ أثناء إعادة الكتابة ، يقلّ الملف، وتُثبَّت السلسلة من جديد عند أول إدخال مُبقى. الاحتفاظ سياسة بيانات، لا سحر: أبق أحدث N، أو أبق كل ما بعد تاريخ، أو أبق خطورة واحدة فقط ، منطق إعادة الكتابة نفسه يخدمها كلها.

🎯 الناتج المتوقع:

kept: 4
3|2025-06-01T10:02:00|WARN|payments|retry #1|…|…
4|2025-06-01T10:03:00|ERROR|payments|charge declined|…|…
5|2025-06-01T10:04:00|ERROR|net|timeout|…|…
6|2025-06-01T10:05:00|WARN|payments|charge recovered|…|…

🩹 إذا لم يعمل : إذا كان kept صفرًا، فقد قلّمت كل شيء (since_seq مرتفع جدًا) ، ضرر لا يشعر به لكن تحقق من العدّ. إذا ما زالت قيم prev للناجين تشير إلى صفوف محذوفة، فإعادة التثبيت if prev != expected: prev = expected ناقصة وستفشل السلسلة في التحقق.

4.2 أعد التحقق من السلسلة المقلّمة

👟 تلميح البداية : شغّل verify() مجددًا ، يجب أن تعود السلسلة المُبقاة خضراء.

# main.py (continued)
print("post-retention verify:", fresh.verify())
from collections import Counter
print(dict(Counter(r[2] for r in fresh.rows())))

الاحتفاظ الجيد يترك سجلًا أصغر لكن ما زال جديرًا بالثقة. إعادة حساب verify() من GENESIS تثبت أن السلسلة المقلّمة متّسقة ذاتيًا، وتُظهر العدّادات بيانات السياسة: إدخالا INFO للدخول غابا، وتلخّص أدلتهما فيما نجا فقط.

🎯 الناتج المتوقع:

post-retention verify: (True, 4)
{'WARN': 2, 'ERROR': 2}

🩹 إذا لم يعمل : إذا أعاد verify() (False, …) بعد التقليم، فأعادت إعادة التثبيت كتابة prev لكن نسيت إعادة حساب تجزئة ذلك الصف نفسه، أو ما زال أول صف مُبقًى يخزّن السَلَف القديم (المحذوف).

4.3 تحقّق من الاحتفاظ

✅ قائمة التحقق

  • retain(3) على سجل من 6 إدخالات يُبقي 4 صفوف بالضبط ويعيد 4.
  • ✅ يعيد الملف المقلَّم التحقق كـ(True, 4).
  • ✅ تعكس العدّادات بعد التقليم الصفوف الباقية فقط.

🤔 سؤال (أسئلة) سقراطي(ة) :

  • يُبقي الاحتفاظ أحدث N الإدخالات ويعيد تثبيتها عند GENESIS. قد يريد مطلب تنظيمي «أُبقي 90 يومًا ثم حُذف» ، ماذا يعني «حُذف» لسلسلة يُفترض أن تكون إلحاق-فقط، ومن يحصل على نسخة قبل تشغيل التقليم؟
  • بعد التقليم، تبدأ قائمة الناجين بـWARN retry #1 ، فأحداث INFO login ok غابت عن الملخص أيضًا. هل تريد إدخال علامة احتفاظ («قُلّمت أحداث INFO اثنان بتاريخ 2025-06-08») يُكتب في السجل، وماذا سيفعل ذلك بالسلسلة؟

الخطوة 5: تصدير الامتثال

تُستهلك سجلات التدقيق ، لوحات المعلومات وأنظمة SIEM وجداول البيانات. تصدّر الخطوة 5 السجل كبيانات يستطيع مستهلك استخدامها، إضافةً إلى ملخص قابل للقراءة البشرية.

5.1 صدِّر JSONL

👟 تلميح البداية : اكتب export_jsonl() معيدة كائن JSON واحدًا لكل صف، بحقول سليمة.

# main.py (continued)
    def export_jsonl(self):
        lines = []
        for row in self.rows():
            lines.append(json.dumps({"seq": int(row[0]), "ts": row[1],
                                     "severity": row[2], "source": row[3],
                                     "message": row[4]}))
        return lines

for line in fresh.export_jsonl():
    print(line)

JSON Lines (.jsonl) هي صيغة التبادل التي تتوقعها لوحات المعلومات ومجمِّعات السجلات: كائن JSON واصف لذاته لكل سطر، كل سطر حدث كامل. تُصدَّر بعد التحقق (4.2)، فتمثّل «المحتوى الذي نثق به»، منفصلة عن صيغة الصفوف الخام التي تعيش فيها السلسلة ، التصدير هو الواجهة، والسلسلة هي شبكة الأمان.

🎯 الناتج المتوقع:

{"seq": 3, "ts": "2025-06-01T10:02:00", "severity": "WARN", "source": "payments", "message": "retry #1"}
{"seq": 4, "ts": "2025-06-01T10:03:00", "severity": "ERROR", "source": "payments", "message": "charge declined"}
{"seq": 5, "ts": "2025-06-01T10:04:00", "severity": "ERROR", "source": "net", "message": "timeout"}
{"seq": 6, "ts": "2025-06-01T10:05:00", "severity": "WARN", "source": "payments", "message": "charge recovered"}

🩹 إذا لم يعمل : إذا أظهرت message تجزئة من 64 محرفًا بدل النص، فصدّرت row[5]/row[6] (حقلَي السلسلة) لا row[4]. إذا أخطأ json.dumps، فحقل ما يحمل غير نصي (كل الحقول هنا نصوص ، تحقق أن seq حُوّل أولًا إلى int).

5.2 الملخص البشري

👟 تلميح البداية : اطبع ملخص امتثال قصيرًا: عدّ الأحداث، وإجماليات كل مصدر وكل خطورة، وحُكم التحقق.

# main.py (continued)
def summary(log):
    rows = log.rows()
    verdict, span = log.verify()
    return f"SIGNALS on {log.path.name}: verified={verdict} events={span} " \
           f"severities={dict(Counter(r[2] for r in rows))}"

print(summary(fresh))

سطر واحد يستطيع مراجع الامتثال أن يقتبسه: «verified=True, events=4, severities=…». ربط الحُكم في السلسلة ذاتها مع العدّادات يمنع اللوحة من عرض أرقام ما كانت السلسلة لتُصدّقها ، التصدير وبيان الثقة يسافران معًا.

🎯 الناتج المتوقع : SIGNALS on audit2.log: verified=True events=4 severities={'WARN': 2, 'ERROR': 2}.

🩹 إذا لم يعمل : إذا كانت verified=False، فجرى التصدير فوق ملف مزوَّر/مُقلَّم بإعادتي تثبيت غير صحيحين. أعد بناء السجل (استعادة 2.3) وأعد التشغيل ، الملخص نزيهٌ بقدر نزاهة السلسلة.

5.3 تحقّق من التصدير

✅ قائمة التحقق

  • ✅ يُصدر export_jsonl() 4 أسطر للسجل المُبقى، برسائل سليمة.
  • ✅ يقرن سطر الملخص verified=True بالعددّات في سلسلة واحدة.
  • ✅ إعادة التفكيك الـJSONL (json.loads) للمرور تعيد حقول البيانات للصفوف مطابقةً.

🤔 سؤال (أسئلة) سقراطي(ة) :

  • يغذّي التصدير لوحة؛ والسلسلة تثبت الملف الذي صدّر عنه. مستهلكٌ رأى مخرجات export_jsonl() فقط بلا سلسلة ، ماذا تشحن بجانب الـJSONL كي يفحصه نظام SIEM لاحق، دون شحن قاعدة كودك كلها؟
  • يبلغ summary عن events=4 وverified=True معًا. لو فشل التحقق، هل تفضّل أن يطبع None للعددّات، أم يطبعها مع تحذير، أم يرفض التشغيل؟ دافع عن اختيارك مع جمهور امتثال في ذهنك.

⚠️ مآزق شائعة

  • انزلاق فهرس الحمولة بواحد. row[:4] يُسقط الرسالة وكل تجزئة معاد حسابها تخالف ما كتبه append صمتًا. الحمولة دائمًا خمسة حقول ([:5])؛ وحقلَا السلسلة هما row[5] (prev) وrow[6] (hash).
  • النمط "w" على السجل الحي. علم open واحد خاطئ يمسح السلسلة في منتصف الجلسة. احجز "w" لـretain وإعادة البناء؛ الإلحاقات الحية يجب أن تكون "a".
  • التقليم دون إعادة تثبيت. اقتطاع الملف مع ترك قيم prev للناجين تشير إلى صفوف محذوفة يجعل السلسلة تفشل في التحقق. أعد حساب prev/التجزئة من GENESIS أثناء إعادة الكتابة، كما يفعل retain.
  • تجزئات تشمل الزمن. digest(str(time.time()), …) يجعل كل تحقق غير حتمي. الطوابع الثابتة في الدليل تُبقي السلاسل قابلة للتكرار؛ إن سجّلت وقت الساعة الحقيقي، يجب أن يكون حقولًا مستقرة ، تُكتب مرة وتُحصَّن فوقها ، لا يُعاد حسابها وقت التحقق.
  • الاستعلام عن أرقام حقول خاطئة. الحقول هي [0]=seq [1]=ts [2]=severity [3]=source [4]=message [5]=prev [6]=hash. التصفية على row[1] تصفّي الطوابع، لا الخطورة.
  • تصدير حقول السلسلة كبيانات. إرسال row[5]/row[6] إلى لوحة يسرّب تجزئات إلى عمود الرسالة. صدّر row[:5] فقط.

ما بنيته للتو

سجل تدقيق مقاوم للتلاعب وقابل للاستعلام ومقلَّم: صفوف أحداث إلحاق-فقط، وسلسلة تجزئة مثبتة عند GENESIS، ودالة verify() تشير إلى الإدخال المزوَّر بالضبط، ومرشحات مع عدّادات خطورة، وتقليم احتفاظ يعيد تثبيت السلسلة، وتصدير امتثال JSONL يحمل ملخصُه حُكم التحقق. الفكرة الجوهرية أن نزاهة التدقيق خاصية تصميمية، لا سلوكًا: لا تعدُ بأنك لن تُزوّر السجل؛ بل تجعل التزوير قابلًا للاكتشاف بسلاسلة كل إدخال إلى سَلَفه وإعادة حساب الوصلة عند الطلب. تلك الحيلة المفردة ، تجزئة واحدة في كل سطر، تتضمن التجزئة السابقة ، هي الصيغة نفسها التي تستخدمها سلاسل الكتل وgit وبيانات النسخ الاحتياطي المزالة التكرار، لأن رسم الكائنات صغير والبرهان رخيص.

شغّل نسخة أكمل دون أي إعداد محلي

examples/audit-logger/ في مستودع الدورة السجل كاملًا كدفتر ، الإلحاق، والتحقق، وعرض التلاعب، والمرشحات، والاحتفاظ، وتصدير JSONL، قابل للتشغيل في Colab/Kaggle/Binder. استنسخ المستودع أو افتحه في Codespace.

إلى أين تذهب من هنا

  • أضف نزاهة بنمط HMAC: وقّع تجزئة كل إدخال بمفتاح سرّي (عبر hmac.new) كي لا يستطيع مؤلّف الإدخالات الصالحة إلا حاملو المفاتيح ، فيُلتقط تلاعب الغرباء الخفيّ أيضًا، لا التعديلات العارضة فقط.
  • صدّر الـJSONL إلى ملف بـ.write_text("\n".join(export_jsonl())) ولوحة تبتلعه، ترسم عدّ ERROR في الساعة من حقل ts.
  • نفّذ tamper_demo() كخطوة تقلب محرفًا واحدًا في السجل عشوائيًا، وتعيد التحقق، وتطبع أي إدخال انكسر ، اختبار ذاتي مدمج للصف.
  • اربط الاحتفاظ بتاريخ (retain_since("2025-06-01T10:03:00")) وسجّل إدخال RETENTION كعلامة في كل تقليم، كي يُشهد على التاريخ المحذوف نفسه.

شارك مشروعك مع الصف

بنيت شيئًا تفخر به؟ examples/student-projects/ معرض لمشاريع قدّمها طلاب آخرون ، وREADME الخاص به يحوي شرحًا كاملًا وودودًا للمبتدئين لإضافة مشروعك عبر pull request، حتى لو لم تستخدم git من قبل قط: عمل fork للمستودع، وإنشاء فرع، وتثبيت ملفاتك، وفتح الـ PR، خطوة بخطوة. لا يُفترض أي خبرة سابقة بـ git.

مرحبًا بك في كتابة Python خارج المتصفح. 🎓

أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.