Paper 2 Network Security Answers

These answers correspond to Paper 2 Network Security Drills.

Verification note: every Python code block in this answer file has been executed locally.

Answer 1: Password Validator

Model answer:

def valid_password(password):
    if len(password) < 8:
        return False
 
    has_digit = False
    for ch in password:
        if ch.isdigit():
            has_digit = True
 
    return has_digit
 
 
print(valid_password("abc123"))
print(valid_password("password"))
print(valid_password("secure9x"))

Expected output:

False
False
True

Mark points:

  • defines a function that accepts the password;
  • checks the minimum length condition;
  • checks each character or uses an equivalent digit test;
  • detects at least one digit;
  • returns False when the password is too short;
  • returns False when the password has no digit;
  • returns True only when both conditions are met.

Common weak answer:

  • using password.isdigit(), which only checks whether the whole password consists of digits.

Answer 2: Login Attempts

Model answer:

def simulate_login_attempts(correct_password, attempts):
    failed = 0
    locked = False
    results = []
 
    for attempt in attempts:
        if locked:
            results.append("LOCKED")
        elif attempt == correct_password:
            results.append("SUCCESS")
            failed = 0
        else:
            failed = failed + 1
            results.append("FAIL")
            if failed == 3:
                locked = True
 
    return results
 
 
print(simulate_login_attempts("open123", ["bad", "wrong", "nope", "open123"]))

Expected output:

['FAIL', 'FAIL', 'FAIL', 'LOCKED']

Mark points:

  • initializes the failed-attempt counter;
  • initializes or represents the locked state;
  • loops through attempts in order;
  • checks locked state before checking the password;
  • appends "LOCKED" for attempts after lockout;
  • resets the counter after a successful login before lockout;
  • increments the counter for wrong passwords;
  • locks the account once the counter reaches three and matches the expected result list.

Common weak answer:

  • allowing the correct fourth attempt after three failures. The question states the account is locked for all later attempts.

Answer 3: Access Log Filter

Model answer:

logs = [
    {"user": "amy", "event": "LOGIN_OK", "ip": "192.0.2.10"},
    {"user": "bo", "event": "LOGIN_FAIL", "ip": "192.0.2.11"},
    {"user": "cy", "event": "LOGIN_FAIL", "ip": "192.0.2.11"},
    {"user": "amy", "event": "LOGOUT", "ip": "192.0.2.10"}
]
 
 
def failed_ips(logs):
    ips = []
    for record in logs:
        if record["event"] == "LOGIN_FAIL":
            ips.append(record["ip"])
    return ips
 
 
print(failed_ips(logs))

Expected output:

['192.0.2.11', '192.0.2.11']

Mark points:

  • loops through the log records;
  • checks the event field;
  • selects only LOGIN_FAIL records;
  • extracts the ip field;
  • preserves order;
  • returns the exact list shown.

Common weak answer:

  • returning usernames instead of IP addresses. The question asks for failed-login IPs.

Answer 4: Simple Hash Check

This constructed "hash:" + entered_password value is only a teaching simulation. It is not a secure hash algorithm.

Model answer:

stored_hash = "hash:letmein"
 
 
def authenticate(entered_password, stored_hash):
    entered_hash = "hash:" + entered_password
    return entered_hash == stored_hash
 
 
print(authenticate("letmein", stored_hash))
print(authenticate("guess", stored_hash))

Expected output:

True
False

Mark points:

  • creates a hash value from the entered password using the specified method;
  • compares hash strings rather than comparing against plaintext storage;
  • returns True when the generated hash matches;
  • returns False when it does not match;
  • produces both expected outputs.

Common weak answer:

  • storing and comparing the plaintext password. The exercise is about checking a derived stored value.

Answer 5: MFA Code

Model answer:

def mfa_login(password, code):
    if password != "river7":
        return "PASSWORD FAILED"
 
    if code != "481926":
        return "CODE FAILED"
 
    return "ACCESS GRANTED"
 
 
print(mfa_login("river7", "481926"))
print(mfa_login("wrong", "481926"))
print(mfa_login("river7", "000000"))

Expected output:

ACCESS GRANTED
PASSWORD FAILED
CODE FAILED

Mark points:

  • checks the password first;
  • returns the password-failure message when the password is wrong;
  • checks the one-time code only after the password is correct;
  • returns the code-failure message when the password is correct but code is wrong;
  • grants access only when both values match;
  • produces the three expected outputs.

Common weak answer:

  • granting access if either the password or code is correct. Multi-factor authentication requires both.

Answer 6: Firewall Rules

Model answer:

rules = [
    {"port": 80, "action": "ALLOW"},
    {"port": 443, "action": "ALLOW"},
    {"port": 22, "action": "BLOCK"}
]
 
 
def check_packet(port):
    for rule in rules:
        if rule["port"] == port:
            return rule["action"]
    return "BLOCK"
 
 
print(check_packet(80))
print(check_packet(22))
print(check_packet(25))

Expected output:

ALLOW
BLOCK
BLOCK

Mark points:

  • represents each firewall rule with port and action data;
  • loops through all rules;
  • compares the packet port with the rule port;
  • returns ALLOW for port 80;
  • returns BLOCK for port 22;
  • uses a default action when no rule matches;
  • default action is BLOCK;
  • produces all three expected outputs.

Common weak answer:

  • allowing unknown ports by default. A conservative firewall rule set usually blocks traffic not explicitly allowed.

Answer 7: IDS Alert

Model answer:

requests = [
    "203.0.113.5",
    "203.0.113.8",
    "203.0.113.8",
    "203.0.113.8",
    "198.51.100.2"
]
 
 
def flag_ips(requests, threshold):
    counts = {}
    for ip in requests:
        if ip not in counts:
            counts[ip] = 0
        counts[ip] = counts[ip] + 1
 
    flagged = []
    for ip in counts:
        if counts[ip] >= threshold:
            flagged.append(ip)
 
    return sorted(flagged)
 
 
print(flag_ips(requests, 3))

Expected output:

['203.0.113.8']

Mark points:

  • initializes a frequency dictionary;
  • loops through every request IP;
  • creates a counter for a new IP;
  • increments the count correctly;
  • compares counts with the threshold;
  • includes IPs whose count is greater than or equal to the threshold;
  • sorts or otherwise returns the required deterministic list;
  • matches the expected flagged IP list.

Common weak answer:

  • flagging every IP that appears at least once. The threshold is part of the IDS rule.

Answer 8: Encryption Demonstration

Model answer:

def shift_encrypt(text, key):
    result = ""
    for ch in text:
        if "A" <= ch <= "Z":
            offset = ord(ch) - ord("A")
            shifted = (offset + key) % 26
            result = result + chr(ord("A") + shifted)
        else:
            result = result + ch
    return result
 
 
print(shift_encrypt("ATTACK AT 9", 2))

Expected output:

CVVCEM CV 9

Mark points:

  • loops through every character;
  • identifies uppercase letters;
  • converts a letter to an alphabet position;
  • adds the key;
  • wraps around using modulo 26;
  • preserves non-uppercase characters unchanged;
  • produces the expected ciphertext for the test.

Common weak answer:

  • describing this as secure real-world encryption. It is only a simple teaching demonstration.

Answer 9: Signature Workflow

Model answer:

def simple_hash(message):
    return str(sum(ord(ch) for ch in message))
 
 
def sign(message, private_key):
    return simple_hash(message) + ":" + private_key
 
 
def verify(message, signature, public_key):
    expected = simple_hash(message) + ":" + public_key
    return signature == expected
 
 
sig = sign("submit marks", "teacher_key")
print(sig)
print(verify("submit marks", sig, "teacher_key"))
print(verify("change marks", sig, "teacher_key"))

Expected output:

1234:teacher_key
True
False

Mark points:

  • computes a hash-like value from the message;
  • signs by combining the message hash with the private-key text as specified;
  • verifies by recomputing the hash from the received message;
  • combines the recomputed hash with the public-key text as specified;
  • returns True for the unchanged message and matching key;
  • returns False for the changed message.

Common weak answer:

  • checking only the key text and not the message hash. A signature workflow should detect message changes.

Real digital signatures use a private key for signing and a corresponding public key for verification. The identical string in this task is only a simplified stand-in for a matching key pair.

Answer 10: Security Test Cases

Model answer:

def valid_password(password):
    if len(password) < 8:
        return False
 
    has_digit = False
    for ch in password:
        if ch.isdigit():
            has_digit = True
 
    return has_digit
 
 
def run_password_tests():
    tests = [
        ("abc123", False),
        ("abcdefgh", False),
        ("abcd1234", True)
    ]
 
    results = []
    for password, expected in tests:
        result = valid_password(password)
        results.append((password, result))
    return results
 
 
print(run_password_tests())

Expected output:

[('abc123', False), ('abcdefgh', False), ('abcd1234', True)]

These results match the expected values in the test-case table: the first password is too short, the second has no digit, and the third satisfies both rules.

Mark points:

  • includes the short-password test case;
  • includes the no-digit test case;
  • includes the valid-password test case;
  • calls the validator for each test input;
  • returns or prints the expected result pairs.

Common weak answer:

  • listing test inputs without expected results. Good test cases state what should happen.