סייבר ואבטחת מידע · siem-detection

KQL למתחילים: שאילתות ראשונות לחקירת אירועים

מאת תאיר מלכא 6 דק׳ קריאהפורסם: 5 באוגוסט 2026
המחשה חזותית מקצועית בנושא KQL למתחילים בתחום SIEM וזיהוי
תשובה מהירה

KQL — Kusto Query Language — היא שפת שאילתות לקריאת וניתוח נתונים במוצרים כגון Azure Monitor ו-Microsoft Sentinel. שאילתה מתחילה בדרך כלל בטבלה וממשיכה בצינור של פקודות: מסננים זמן ואירועים, בוחרים או יוצרים שדות, מסכמים לפי משתמש או נכס ומציגים את התוצאות הרלוונטיות לחקירה. המפתח ללמידה הוא להתחיל בשאלה אחת ולבנות את השאילתה שלב אחר שלב.

אנליסט SOC אינו צריך לזכור מאות פקודות כדי להתחיל לעבוד עם KQL. הוא צריך להבין את מודל החשיבה: באיזו טבלה נמצא המידע, מהו טווח הזמן, אילו שורות רלוונטיות, אילו שדות דרושים, וכיצד לסכם את התוצאות כדי לענות על שאלת חקירה.

KQL היא שפה לקריאה וניתוח. היא אינה SQL, אף שיש מושגים דומים. הנתונים זורמים משמאל לימין דרך Pipe — הסימן | — וכל שורה מקבלת את התוצאה של השורה הקודמת. כך ניתן לבנות חיפוש קטן, לבדוק תוצאה, ולהוסיף שלב נוסף בלי לכתוב הכול בבת אחת.

הדוגמאות במדריך משתמשות בשמות טבלאות ושדות נפוצים, אך Schema משתנה בין Workspaces ומקורות. לפני שמעתיקים Query, פתחו כמה רשומות, בדקו את השדות בפועל והתאימו את הלוגיקה. כל הנתונים בתרגיל הם מדומים.

מבנה בסיסי של Query

השורה הראשונה מציינת בדרך כלל Table. כל Pipe מוסיף Operator. לדוגמה, Query שמציג עשר התחברויות אחרונות מטבלת SigninLogs:

SigninLogs
| where TimeGenerated > ago(24h)
| project TimeGenerated, UserPrincipalName, IPAddress, ResultType
| sort by TimeGenerated desc
| take 10

קראו את השאילתה כמשפט: קח את SigninLogs, שמור רק אירועים מהיממה האחרונה, הצג ארבעה שדות, מיין מהחדש לישן וקח עשר שורות. סדר השלבים חשוב הן להבנה והן לביצועים. סינון מוקדם מפחית את כמות הנתונים שעוברת לשלבים הבאים.

שלב 1: הכירו את הטבלה

לפני חקירה, הריצו כמה שורות כדי לראות Schema ודוגמאות. הפקודה take מצוינת ללמידה, אך אינה מבטיחה את האירועים האחרונים אלא אם ממיינים. אפשר להשתמש ב-project כדי לצמצם את התצוגה וב-project-away כדי להסתיר שדות שאינם דרושים.

SigninLogs
| take 5

שימו לב לשדות מסוג datetime, string, dynamic ו-int. שדה dynamic עשוי להכיל JSON או מערך ודורש לעיתים parse_json, mv-expand או גישה למאפיין פנימי. אל תניחו ש-ResultType זהה לכל מקור. בתיעוד הטבלה או בדוגמאות בפועל תבדקו מה משמעות הערכים.

שלב 2: סינון באמצעות where

where הוא כלי החקירה המרכזי. אפשר לסנן לפי זמן, משתמש, IP, תוצאה או טקסט. עדיף להתחיל בטווח זמן צר ולהשתמש בהשוואות שמתאימות לסוג השדה.

SigninLogs
| where TimeGenerated between (datetime(2026-08-01 08:00:00) .. datetime(2026-08-01 12:00:00))
| where UserPrincipalName =~ "student@contoso.example"
| project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName, ResultType

האופרטור =~ משווה מחרוזת ללא תלות באותיות גדולות וקטנות. לחיפוש חלקי אפשר להשתמש ב-contains, has או startswith. במקרים רבים has יעיל ומדויק יותר לחיפוש מונח שלם. הימנעו מסינון רחב מאוד עם contains כאשר אפשר להשתמש בשדה מובנה.

שלב 3: בחירת שדות ויצירת הקשר

project מחזיר רק את השדות שנבחרו. extend יוצר שדה חדש בלי להסיר את הקיימים. הדבר שימושי לתוויות, חישובים ונרמול מקומי.

SigninLogs
| where TimeGenerated > ago(24h)
| extend Outcome = iff(tostring(ResultType) == "0", "Success", "Failure")
| project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName, Outcome

בעת חקירה, העדיפו שמות שדות שמבהירים משמעות. אפשר להשתמש ב-project-rename כדי לשנות שם לתצוגה, אך אל תטשטשו את מקור הנתון. Ticket מקצועי צריך לציין מאיזו Table ושדה הגיע כל ממצא חשוב.

שלב 4: summarize — להפוך אירועים לתמונה

summarize מקבץ אירועים ומחשב Aggregations. ניתן לספור, למצוא זמן ראשון ואחרון, לאסוף ערכים, לחשב מספר משתמשים ייחודיים ולבנות Baseline. BY מגדיר את הקבוצות.

SigninLogs
| where TimeGenerated > ago(24h)
| summarize Attempts=count(),
            FirstSeen=min(TimeGenerated),
            LastSeen=max(TimeGenerated),
            Applications=make_set(AppDisplayName, 10)
  by UserPrincipalName, IPAddress
| sort by Attempts desc

make_set שימושי להצגת ערכים שונים, אך רשימות גדולות מכבידות ומקשות על הקריאה. הגבילו את מספר הפריטים. dcount נותן ספירה מקורבת של ערכים ייחודיים ומתאים לניתוח רחב; distinct מחזיר שילובים ייחודיים כאשר צריך לראות אותם.

countif ו-dcountif

בחקירות Authentication רוצים לעיתים לספור הצלחות וכשלונות באותה קבוצה. countif סופר רק שורות שעומדות בתנאי:

SigninLogs
| where TimeGenerated > ago(24h)
| summarize Failures=countif(tostring(ResultType) != "0"),
            Successes=countif(tostring(ResultType) == "0"),
            UniqueUsers=dcount(UserPrincipalName)
  by IPAddress
| where Failures >= 5
| sort by Failures desc

התוצאה היא Lead. היא אינה מוכיחה שהצלחה התרחשה אחרי הכשלונות, וש-IP משותף יכול להיות NAT, Proxy או VPN. יש לפתוח את האירועים הגולמיים ולבנות Timeline לפני Classification.

חלונות זמן באמצעות bin

bin מקבץ datetime למקטעים קבועים. הדבר מאפשר לראות Burst של פעילות במקום לסכם יום שלם. בחרו Span לפי התנהגות: Password Spray עשוי להימתח לאורך זמן כדי להימנע מסף; Brute Force נגד חשבון אחד עשוי להיות מהיר.

SigninLogs
| where TimeGenerated > ago(24h)
| summarize Failures=countif(tostring(ResultType) != "0"),
            Successes=countif(tostring(ResultType) == "0"),
            Users=dcount(UserPrincipalName)
  by IPAddress, bin(TimeGenerated, 30m)
| where Failures >= 10 and Successes >= 1
| sort by TimeGenerated desc

העמודה TimeGenerated בתוצאה מייצגת תחילת ה-Bin. כדי לראות את הסדר המדויק, השתמשו בתוצאה כדי לבחור IP וחלון, ואז הריצו Query שני שמציג את האירועים לפי זמן.

let — להפוך Query לקריאה

let מאפשר לתת שם לערך או לטבלה זמנית. הוא מועיל להגדרת Time range, Threshold או קבוצת נתונים שמשתמשים בה כמה פעמים.

let Lookback = 24h;
let FailureThreshold = 10;
SigninLogs
| where TimeGenerated > ago(Lookback)
| summarize Failures=countif(tostring(ResultType) != "0"),
            Successes=countif(tostring(ResultType) == "0"),
            Users=dcount(UserPrincipalName),
            UserList=make_set(UserPrincipalName, 20)
  by IPAddress, bin(TimeGenerated, 30m)
| where Failures >= FailureThreshold and Successes >= 1
| project TimeGenerated, IPAddress, Failures, Successes, Users, UserList
| order by Failures desc

שמות ברורים והערות מקטינים טעויות. ב-KQL ניתן להוסיף Comment באמצעות //. Query שמתוכנן להפוך ל-Analytics rule צריך לתאר את מטרת הזיהוי, ההנחות, המקורות, גרסה ו-Owner.

תרגיל מעשי: כשלונות ולאחריהם Login מוצלח

במעבדה, הפעילו את Query הסיכום על נתונים מדומים. בחרו שורה שבה קיימים Failures ו-Successes. לאחר מכן בצעו Drill-down:

let TargetIP = "203.0.113.25";
let WindowStart = datetime(2026-08-01 09:00:00);
SigninLogs
| where TimeGenerated between (WindowStart .. WindowStart + 30m)
| where IPAddress == TargetIP
| extend Outcome = iff(tostring(ResultType) == "0", "Success", "Failure")
| project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName, Outcome, ResultDescription
| order by TimeGenerated asc

ענו על שאלות: האם הכשלונות פגעו במשתמש אחד או רבים? האם ההצלחה שייכת לאותו משתמש? האם ה-IP מוכר כ-VPN? האם MFA הופעל? האם קיימת פעילות נוספת לאחר ההצלחה? רק לאחר איסוף ההקשר ניתן לקבוע אם מדובר ב-Password Spray, טעות משתמש, שירות ישן או פעילות מאושרת.

אופטימיזציה ובטיחות עבודה

  • סננו זמן מוקדם והשתמשו בטבלה המדויקת ביותר.
  • סננו בשדות מובנים במקום לבצע חיפוש טקסט על Raw data.
  • הציגו רק שדות דרושים באמצעות project.
  • הימנעו מ-join על נפחים גדולים לפני שבדקתם חלופות כגון lookup או summarize.
  • הגבילו make_set ו-make_list.
  • בדקו Query על טווח קטן לפני הרחבה.
  • תעדו הנחות, Thresholds ומשמעות ערכי Result.
  • אל תהפכו Query ל-Detection לפני שבדקתם True/False/Benign Positives.

טעויות נפוצות

  • להעתיק Query בלי לבדוק את Schema המקומי.
  • לסכם יום שלם ולהסיק שהיה רצף אירועים.
  • לשכוח טווח זמן ולסרוק נפח גדול ללא צורך.
  • להשתמש בשם שדה שונה בין שני מקורות כאילו הוא אחיד.
  • להתייחס ל-Query result כהוכחה לתקיפה.
  • ליצור רשימות ענק באמצעות make_set.
  • לכתוב Query ארוך אחד במקום לבדוק כל שלב.

סיכום ו-CTA

פתחו סביבת Log Analytics או מעבדה עם נתונים מדומים ובנו Query בשלושה צעדים: הציגו חמש שורות, סננו אירוע אחד וסכמו לפי משתמש או IP. שמרו כל גרסה והסבירו מה היא עונה. לאחר מכן עברו למדריך חקירת Incident ב-Microsoft Sentinel כדי לראות כיצד Query משתלבת בתוך Case אמיתי.

שאלות ותשובות

האם KQL דומה ל-SQL?

יש מושגים משותפים כמו סינון, Projection ו-Aggregation, אך התחביר ומודל ה-Pipe שונים. כדאי ללמוד KQL כשפה בפני עצמה.

מה ההבדל בין where ל-search?

where מסנן לפי ביטוי ושדות מוגדרים ומועדף ברוב החקירות. search יכול לחפש ערכים בצורה רחבה יותר, אך לעיתים פחות מדויק ויעיל.

האם Query יכולה לשנות או למחוק לוגים?

KQL בהקשר של Log Analytics ו-Sentinel משמשת לקריאה וניתוח. פעולות ניהול ו-Ingestion מתבצעות באמצעות מנגנונים אחרים והרשאות מתאימות.

מה ההבדל בין summarize ל-distinct?

distinct מחזיר שילובים ייחודיים. summarize מקבץ ומחשב ערכים כגון count, min, max או make_set.

כיצד יודעים איזו Table לחפש?

מתחילים ב-Data connector ובתיעוד Schema, משתמשים בחיפוש Tables ב-Log Analytics ובודקים דוגמאות אירועים. אותו Use Case יכול להשתמש בטבלאות שונות בין ארגונים.

מקורות

רוצים לבדוק אם המסלול מתאים לכם?

השאירו פרטים ונציג של HPI יחזור אליכם לשיחת התאמה קצרה וללא התחייבות.

פרטייך נשמרים באופן מאובטח.

ללימודי SOC וסייבר במסגרת תוכנית Cybersecurity & AI

רוצים לשמוע פרטים על התוכנית? השאירו פרטים ונחזור אליכם.

על הכותב
תאיר מלכא
מייסד ומנכ״ל HPI ומרצה לסייבר

תאיר מלכא הוא מייסד ומנכ״ל HPI – המכללה למקצועות ההייטק, ומרצה לסייבר במסלולי ההכשרה של המכללה. במסגרת תפקידיו הוא אחראי על בניית תוכניות הלימוד המקצועיות של HPI בתחומי סייבר, IT ורשתות, ומוביל את הליווי המקצועי של הסטודנטים לאורך המסלול.

מאמרים קשורים