Offline security exercise / source reading
Login triage: original guided practice
Read the supplied instructions here, then download the complete project to use the Python utilities and training materials. These are guided practice materials; reference checks do not assess independent understanding.
Archive folder: jeremy-bryant-pages/labs/login-triage/
Lab instructions
labs/login-triage/README.md
# Login triage: original guided practice Practise reading sign-in records, reconstructing a timeline, checking an alert rule, and writing a short handover. A **security operations centre (SOC)** is a team that reviews possible security problems. **Triage** means deciding what deserves attention first and what evidence is needed next. These are original guided practice materials for a beginner. The records, organisation, accounts, notes and future timestamps are fictional. This lab is independent learner practice, not an assessed exercise or evidence of professional SOC experience. Following the worked example does not establish independent mastery, and the supplied utility is not a claim that Jeremy independently built a production security tool. ## Start here Use Python 3.9 or newer. Only Python's standard library is required: no packages, sign-in, network traffic or live authentication. Open a terminal in this folder. On Windows, `py` can replace `python` if that is your installed Python command. ```sh python lab.py --help python lab.py verify python lab.py timeline python lab.py timeline --user practice-staff-041 python lab.py triage python lab.py verify --report runs/report.json ``` Read the three invented account notes in [`fixture/context.json`](fixture/context.json). Write your own interpretation before opening the [worked example](WORKED-EXAMPLE.md). Continue with the [practice drills](PRACTICE.md). `triage` writes `runs/report.json`. Open that file in a text editor to inspect the counted events and linked successes. Repeating a command that writes an existing output is refused; give `--output` a fresh name to preserve earlier work. Relative **output** paths are resolved from this lab folder. Relative **input** paths are resolved from your terminal's current folder, so run the examples here. ## Compact command contract `lab.py` is the single maintained command entry point. It reads local synthetic files only. Generated cases, reports and test scratch files stay under this folder's ignored `runs/` directory. It will not overwrite existing outputs. Tests remove only their own temporary subdirectories. No command changes the checked-in fixture or performs account actions. | Command | Input | Result | | --- | --- | --- | | `generate --seed 72 --scenario baseline` | Numeric seed and scenario name | A new `runs/seed-72-baseline/` with CSV, JSONL, context and manifest | | `timeline [--input FILE] [--user NAME] [--source-ip IP] [--outcome failure]` | Fixture CSV by default; exact-value filters can be combined | Sorted selected events printed to the terminal; no file written | | `triage [--input FILE] [--threshold 5] [--window-seconds 300] [--success-seconds 120] [--output runs/report.json]` | CSV or JSONL | One JSON report with settings, evidence event IDs and input hash | | `verify [--case DIRECTORY] [--report FILE]` | Fixture directory by default | Recreates the seed's expected files, compares every byte and optionally recalculates the report | | `test` | Supplied fixture and temporary synthetic cases | Timing, grouping, invalid-input and integrity checks; exit status 0 when all pass | Exit status 0 means the command completed, 1 means tests failed, and 2 means invalid input or a file problem. A status is a number the terminal can use to distinguish completion from failure. `verify` checks file integrity and calculation consistency; it does **not** assess your explanation or validate the rule for a real organisation. A hash is not a digital signature or proof of who authored a file. ## What the rule actually counts The default **threshold** is five failures. The default **window** is 300 seconds (five minutes). For each distinct failure timestamp, the program counts only failures for the **same account and same source address**, between `timestamp − 300 seconds` and that timestamp, including both endpoints. Earlier failures outside that window are excluded. Successes do not count as failures and do not reset the window. A qualifying window is a **candidate** for review. Each qualifying end time is reported, so two overlapping windows can describe one sequence. Do not count the two windows as two proven incidents. Records are sorted by timestamp; file order does not establish event order. All failures sharing a second are counted together. Their event IDs are labels, not evidence of subsecond order. A **linked success** means the same account and source address had a success strictly after a qualifying window ended, within the next 120 seconds, including the upper boundary. A success in the same second is not linked because the data cannot establish which event came first. `--success-seconds 0` disables links. A timing link does not establish the same person, a successful attack, or what happened after sign-in. This is a deliberately small **heuristic**: a useful rule of thumb, not proof of compromise. It can miss failures spread across several addresses or accounts. A shared internet address can also represent several people. A lack of candidates means only that this rule found none in these records; it does not establish that an account is safe. ## Reading the evidence The fixture contains 19 events in deliberately shuffled order. CSV and JSONL contain the same events in two formats; they are alternatives, not two independent evidence sources. | Field | Plain-language meaning | | --- | --- | | `event_id` | Unique synthetic record label, such as `SYN-0001` | | `timestamp` | Invented time in UTC, written `YYYY-MM-DDTHH:MM:SSZ`; UTC is a common time reference and `Z` marks it | | `user` | Fictional account label beginning `practice-` | | `source_ip` | Address recorded as the source of the attempt; all values use reserved documentation ranges | | `outcome` | `failure` or `success` according to this fictional log | | `reason` | `invalid_password` for a failure or `accepted` for a success; it does not identify the person typing | | `synthetic` | The literal text `true`, making each row explicitly fictional | **CSV** is a text table with comma-separated fields. **JSONL** is a text file with one structured JSON record per line. **JSON** names fields so a program or reader can inspect them. An **IP address** is a network address; these documentation values do not identify real hosts. A **service account** is used by an automated job rather than a person. A **stale password** is an old saved password that no longer works. The parser rejects missing or unexpected fields, duplicate event IDs, invalid dates, unsupported outcomes, non-documentation addresses and rows without explicit synthetic labels. These constraints help keep this exercise scoped; they cannot prove that arbitrary user-edited text contains no personal data. Use the supplied generator and fictional edits only. ## Evidence, interpretation and next action Keep these three parts separate in your notes: 1. **Observed:** name the account, source address, UTC interval, event IDs and counts. State the exact rule settings. 2. **Interpretation:** describe a plausible explanation and what remains unknown. A helpdesk note supports an explanation but is not independent verification of the actor. 3. **Next check:** request the missing evidence that could change your decision, such as confirmation from the account owner, device records, an additional sign-in verification record, or actions taken after the sign-in. In this exercise, write the request only. A **false positive** is a rule match that turns out not to be the suspected security problem. It may still reveal an operational fault. An **escalation** asks a more experienced reviewer to investigate; a **handover** provides enough evidence for that person to continue. Neither requires claiming an attacker was identified. ## Reproducibility and scope A **seed** is a number that produces the same fictional case again. Change the seed to vary account labels, addresses, times and file order. Use `baseline`, `late-success` or `distributed` to change the sequence's behaviour. [`fixture/manifest.json`](fixture/manifest.json) records synthetic provenance, seed, scenario, generator version and SHA-256 hashes of the data and context. **SHA-256** produces a fingerprint of file bytes; a changed file normally has a different fingerprint. The checked-in fixture is seed 41, baseline, generator version 1.0.0. `verify` compares it with regenerated content, including the manifest, rather than merely trusting hashes written in the manifest. Preserve LF line endings when copying fixture files. Keep your notes and new cases in `runs/`; it is excluded from Git, the repository history system. No personal data, passwords or real authentication logs are needed. Run `python lab.py test` after changing the tool. Passing software checks and following the worked answer are distinct from factual judgement or independent learner performance. This lab has no external grader, no badge and no automatic mastery claim.
Worked example
labs/login-triage/WORKED-EXAMPLE.md
# Worked example: seed 41, baseline Try the fixture yourself before reading this answer. This is a model explanation for guided practice, not a record of learner performance. ## First reconstruct the timeline ```sh python lab.py timeline --user practice-staff-041 python lab.py timeline --user practice-editor-041 python lab.py timeline --user practice-sync-041 ``` All times below are invented UTC times on **7 April 2032**. The CSV is shuffled, so reading rows from top to bottom would give the wrong sequence. | Account and source | Observed sequence | Interpretation and remaining gap | | --- | --- | --- | | `practice-staff-041`, `192.0.2.52` | `SYN-0001`–`SYN-0006`: six failures at 09:24:00, 09:24:40, 09:25:20, 09:26:00, 09:26:40 and 09:27:20. `SYN-0007`: success at 09:28:20. | Repeated failures followed by success warrant review. Password guessing is possible; repeated user error is also possible. There is no user confirmation or device evidence. | | `practice-editor-041`, `198.51.100.108` | `SYN-0008` and `SYN-0009`: failures at 09:25:10 and 09:25:35. `SYN-0010`: success at 09:26:10. Later successes at 09:40:40, 09:54:00 and 10:04:00. | Two mistakes followed by a successful attempt agree with the invented helpdesk note. There is no five-failure match. This supports lower priority, not certainty about who signed in. | | `practice-sync-041`, `203.0.113.157` | `SYN-0011`–`SYN-0016`: six failures from 09:24:30 to 10:19:30, exactly 11 minutes apart. No success appears. | The invented change note says an automated job still has an old password. This supports an operational fault. At most one of these failures fits in any five-minute window. | ## Compare the rule with your count ```sh python lab.py triage --output runs/worked-report.json python lab.py verify --report runs/worked-report.json ``` Default settings are five failures, a 300-second failure window and a 120-second later-success link. The result is **two qualifying windows across one account/address pair**: | Window end | Inclusive failure window | Counted failures | Linked success | | --- | --- | --- | --- | | 09:26:40 | 09:21:40–09:26:40 | `SYN-0001`–`SYN-0005`, count 5 | `SYN-0007` at 09:28:20, 100 seconds later | | 09:27:20 | 09:22:20–09:27:20 | `SYN-0001`–`SYN-0006`, count 6 | `SYN-0007` at 09:28:20, 60 seconds later | These windows overlap and use the same later success. They describe one observed sequence and do not establish two incidents. The first window starts before the first available event because the start is a calculation from the window end. Do not infer that records exist for that earlier time. Now widen the failure window to one hour: ```sh python lab.py triage --window-seconds 3600 --output runs/worked-wide-report.json python lab.py verify --report runs/worked-wide-report.json ``` The result is **four qualifying windows across two pairs**. The service account now reaches five and six failures within an hour. Its two windows have no linked success. If every match were labelled an attack, the service's stale saved password would be a false positive for that claim. The same records can deserve operational follow-up without evidence of a security incident. ## Example handover > Review requested: the synthetic account `practice-staff-041` has six failed sign-ins from `192.0.2.52` between 09:24:00 and 09:27:20 UTC on 7 April 2032 (`SYN-0001`–`SYN-0006`), followed by a success at 09:28:20 (`SYN-0007`). With a five-failure/300-second rule and a 120-second success link, two overlapping windows describe the same sequence. The success is 60 seconds after the final failure. This could reflect password guessing or repeated legitimate errors; these records do not establish the actor or compromise. Please confirm the activity with the account owner and review device, additional sign-in verification and post-sign-in activity records. The editor's two failures align with the supplied typo note; the automated sync account's 11-minute failures align with its stale-password change note and need operational follow-up. No account actions were taken in this exercise. The handover names evidence, retains uncertainty, separates likely explanations, and asks for checks that could change the decision. It does not claim that a successful attack or an independently verified benign explanation has been proven.
Practice notes
labs/login-triage/PRACTICE.md
# Repeat the exercise with less help Keep a short note for each attempt inside `runs/`. Record the seed, scenario, rule settings, time spent, help used and one correction you made. These are self-reported practice notes, not assessed results. Do not replace an earlier attempt with an improved answer; a separate note makes progress easier to inspect. ## 1. Guided pass Use the checked-in fixture and the command reference in `README.md`. 1. Run `verify` to establish which supplied files you are using. 2. Use `timeline --outcome failure` to select failures, then filter each account separately. **Filtering** means selecting records that match a condition. Remove the outcome filter before checking for a success. 3. Write the first and last relevant UTC times and record IDs. Count the failures in a five-minute interval yourself. 4. Read the invented account notes. Write one observed fact and one possible explanation for each account. 5. Run `triage`, compare its event IDs with yours, and explain why two overlapping windows do not establish two incidents. 6. Draft a short handover with evidence, uncertainty and the next check. Compare it with `WORKED-EXAMPLE.md`, then note your corrections. ## 2. Hints-only pass Create a fresh case: ```sh python lab.py generate --seed 72 python lab.py verify --case runs/seed-72-baseline ``` Read its `context.json` and inspect `auth.csv` or `auth.jsonl`. Without the worked example, identify the sequence that deserves review, explain the two other patterns, and write a handover. Then run the tool against this input and verify the report: ```sh python lab.py triage --input runs/seed-72-baseline/auth.csv --output runs/practice-72-report.json python lab.py verify --case runs/seed-72-baseline --report runs/practice-72-report.json ``` Use these hints only if needed: sort before counting; keep account and source address together; distinguish the number of failures from the number of reported windows; compare retry spacing with the window length; a later success still leaves the actor unknown. Record which hints you used. ## 3. No-notes pass Generate seed 109 with the baseline scenario. Close the worked example and previous attempts. Use only that case's data and context. Tool `--help` is allowed for command syntax; record that you used it. Produce: - A chronological account of the sequence you would escalate, with event IDs and UTC times. - A reasoned priority for each of the three accounts, including a competing explanation. - A statement of what the logs cannot establish and the specific next evidence you would request. - A concise handover and a separate list of unknowns. Only after saving the attempt, compare it with the tool output and the self-check below. A seed changes labels and time but preserves the baseline pattern; this tests recall and basic transfer, not broad detection skill. ## 4. Changing-scenario drills Predict the effect before running triage. Use a new report filename for every run. The examples below name the input so that the tool cannot silently use the default fixture instead. ### Success arrives later ```sh python lab.py generate --seed 86 --scenario late-success python lab.py triage --input runs/seed-86-late-success/auth.csv --output runs/late-default.json python lab.py triage --input runs/seed-86-late-success/auth.csv --success-seconds 600 --output runs/late-long-link.json ``` Explain why the failure matches remain the same while the linked-success list changes. Find the actual later success in the full timeline. Does the absence of a default link mean no success happened? Does a longer link establish the same actor? ### Failures use several addresses ```sh python lab.py generate --seed 93 --scenario distributed python lab.py triage --input runs/seed-93-distributed/auth.csv --output runs/distributed.json ``` This changes the staff sequence to six failures spread across three documentation addresses. Reconstruct it by account with `timeline`. Explain why the same-account-and-address rule misses it. Describe an additional account-level review in plain language; do not change the conclusion to “safe” because the report has zero candidates. Account-wide grouping can also combine unrelated users of a shared account, so it needs context. ### A wider window and a different threshold ```sh python lab.py triage --window-seconds 3600 --output runs/wide-window.json python lab.py triage --threshold 7 --output runs/higher-threshold.json python lab.py triage --threshold 2 --output runs/lower-threshold.json ``` These three commands deliberately use the checked-in fixture. Identify the tradeoff: a higher threshold may miss concerning activity; a lower threshold or wider window may add ordinary mistakes and service faults. Which matches are useful, which would be false positives for an attack claim, and which extra evidence would help? No single setting is universally correct. For each scenario, run `verify --case CASE_DIRECTORY --report REPORT_FILE` after triage; use `fixture` as the case for the final three commands. Explain what a PASS does and does not tell you. ## Self-check after saving your answer Use these prompts for reflection rather than a numerical score: - Did I cite specific records and UTC times instead of merely repeating the tool's label? - Did I count only failures inside the chosen window, and identify overlaps? - Did I inspect the complete sequence after filtering failures? - Did I separate a log observation, an invented context note and my inference? - Did I explain a plausible benign cause without declaring it independently proven? - Did I avoid treating a linked success or an empty report as a final security verdict? - Could another reader continue from my handover without guessing which records I mean? Suggested check values, to read only after predicting: baseline defaults produce two windows and one pair; late-success defaults produce two windows with no linked successes, while a 600-second link includes the later success in both; distributed defaults produce zero windows. On the fixture, a 3600-second window produces four windows and two pairs, threshold 7 produces zero, and threshold 2 produces six windows across two pairs. These values check calculations, not the quality of your interpretation.