Skip to content

Latest commit

 

History

History
268 lines (190 loc) · 12.3 KB

File metadata and controls

268 lines (190 loc) · 12.3 KB

Object-Oriented Programming

Modelling the entities a security tool works with — targets, ports, results, reporters — as classes, so the code stays organised as it grows from a script into a tool.

Part of the [[Readme|Python for Security Professionals]] course.

Overview

Object-Oriented Programming (OOP) bundles data (attributes) and the behaviour that acts on that data (methods) into a single unit called an object. A class is the blueprint; an object (or instance) is a concrete thing built from that blueprint.

In Python everything is an object — strings, lists, sockets, and file handles all expose attributes and .method() calls. Writing your own classes lets you model the entities a security tool deals with so that state and the operations on it travel together instead of being threaded through every function call.

This module builds the four classic pillars one at a time — Encapsulation, Inheritance, Abstraction, and Polymorphism — and finishes with the dunder methods that hook your objects into Python's own syntax.

[!note] Module hub OOP is one paradigm, not a requirement. Small scripts are often clearer as plain functions — see [[Methods-and-Functions/Readme|Methods & Functions]]. Reach for classes when you have state and behaviour that travel together, or many objects of the same shape.

Learning Objectives

By the end of this module you will be able to:

  • Define classes, construct instances, and initialise state in __init__.
  • Distinguish class attributes from instance attributes and avoid the shared-mutable-default bug.
  • Choose correctly between instance, class, and static methods.
  • Build a shallow inheritance hierarchy and extend behaviour with super().
  • Encapsulate state behind validated properties, and explain why __private is not a security control.
  • Design a plugin interface using duck typing or an abstract base class.
  • Implement the dunder methods that make objects printable, comparable, and usable in with blocks.

Prerequisites

  • [[Methods-and-Functions/Readme|Methods & Functions]] — functions, arguments, return values, and scope.
  • [[Python-Objects-and-Data-Structure-Basics/Readme|Python Objects & Data Structure Basics]] — lists, dictionaries, and mutability.
  • [[Python-Statements-and-Control-Flow/Readme|Python Statements & Control Flow]] — loops and conditionals.

Topics Covered

Ordered so each topic builds on the last:

  • [[Understanding-Objects]] — what an object is, and why everything in Python is one.
  • [[Defining-Classes]] — the class keyword, __init__, and cheap, side-effect-free construction.
  • [[Class-Attributes]] — shared defaults, attribute lookup order, and the mutable-class-attribute trap.
  • [[Instance-Attributes]] — per-object state, __dict__, and declaring every attribute in __init__.
  • [[Instance-Methods]] — the default method flavour and where a class's invariants live.
  • [[Class-Methods]] — @classmethod, alternate constructors, and why factories must return cls(...).
  • [[Static-Methods]] — @staticmethod helpers, and when a plain module is the better answer.
  • [[Inheritance]] — the is-a relationship, the MRO, and keeping hierarchies shallow.
  • [[Method-Overriding]] — replacing versus extending inherited behaviour.
  • [[Super-Function]] — cooperative delegation along the MRO, including with mixins.
  • [[Encapsulation]] — underscore conventions, name mangling, and validated properties.
  • [[Polymorphism]] — duck typing, abstract base classes, and plugin interfaces.
  • [[Magic-Dunder-Methods]] — __str__, __repr__, __eq__, __len__, and the context-manager protocol.

Two closely related topics are covered in [[Advanced-Python-Data-Structures/Readme|Advanced Python Data Structures]] rather than repeated here:

  • [[Advanced-Python-Data-Structures/Data-Classes|Data Classes]] — @dataclass generates __init__, __repr__, and __eq__ for data-holder classes.
  • [[Advanced-Python-Data-Structures/Property-Decorators|Property Decorators]] — @property getters, setters, and computed attributes in depth.

Practical Exercises

Work through these in order; each builds on the previous one.

  1. Model a target. Write a Target class holding ip, hostname, and open_ports, validating the IP in __init__.
  2. Add behaviour. Give it record(port) that ignores duplicates and is_web_host() that checks for 80/443/8080.
  3. Add a factory. Write a from_cidr() class method returning a list of Target objects for a /29 lab range.
  4. Build the hierarchy. Create a BaseScanner with a run() that performs a scope check then calls an overridable probe(); subclass it as TCPScanner.
  5. Make it printable. Add __repr__ and __eq__ so results de-duplicate correctly in a set().
  6. Make it pluggable. Define a ScanPlugin(ABC) and implement two plugins the engine drives polymorphically.
  7. Make it safe. Add a context manager that guarantees every socket is closed even when a probe raises.

Security Applications

The recurring shape of a Python security tool maps directly onto classes:

Scanner          drives the engagement, owns configuration and the plugin list
├── Target       one host: ip, hostname, os_guess
├── Port         a validated port number, with is_privileged
├── Result       what a probe found: port, state, banner, evidence
└── Reporter     renders results as text, JSON, or Markdown

A minimal, complete version:

#!/usr/bin/env python3
"""Class-based skeleton for a small, authorized-use port scanner."""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field


class Port:
    """A validated TCP/UDP port number."""

    def __init__(self, number):
        if not 0 < number <= 65535:
            raise ValueError(f"invalid port: {number}")
        self.number = number

    @property
    def is_privileged(self):
        return self.number < 1024

    def __repr__(self):
        return f"Port({self.number})"


@dataclass
class Result:
    """What a single probe found."""

    port: int
    state: str
    banner: str = ""


@dataclass
class Target:
    """One host in scope for this engagement."""

    ip: str
    hostname: str = "unknown"
    results: list = field(default_factory=list)

    def record(self, result):
        self.results.append(result)

    @property
    def open_ports(self):
        return [r.port for r in self.results if r.state == "open"]


class Probe(ABC):
    """Interface every scan plugin must implement."""

    @abstractmethod
    def name(self):
        ...

    @abstractmethod
    def run(self, target, port):
        ...


class ConnectProbe(Probe):
    """Stand-in for a real TCP connect probe."""

    def name(self):
        return "tcp-connect"

    def run(self, target, port):
        state = "open" if port.number in (22, 80) else "closed"
        return Result(port=port.number, state=state)


class Reporter:
    """Renders results. Subclass to add a format."""

    def render(self, target):
        ports = ", ".join(str(p) for p in sorted(target.open_ports))
        return f"{target.ip}: {ports or 'no open ports'}"


class Scanner:
    """Drives probes across a target's ports."""

    def __init__(self, probes, reporter=None):
        self.probes = probes
        self.reporter = reporter or Reporter()

    def scan(self, target, ports):
        for port in ports:
            for probe in self.probes:            # polymorphic dispatch
                target.record(probe.run(target, port))
        return self.reporter.render(target)


def main():
    target = Target("127.0.0.1", hostname="localhost")
    ports = [Port(n) for n in (22, 80, 443)]
    scanner = Scanner(probes=[ConnectProbe()])
    print(scanner.scan(target, ports))


if __name__ == "__main__":
    main()
127.0.0.1: 22, 80

Every pillar is visible here: Port encapsulates validation, Probe defines an abstract interface, ConnectProbe supplies the polymorphic implementation, and Reporter is designed to be inherited from. [[Security-Tool-Development/Security-Tool-Architecture|Security Tool Architecture]] takes the same design and turns it into an installable package.

[!warning] Authorized use only The skeleton above targets 127.0.0.1. Only scan systems you own or are explicitly authorized to test.

Common Mistakes

  • Mutable class attributes used as per-object state — one list shared by every instance, which in a scanner means one host's findings appearing under another's.
  • Skipping super().__init__() in a subclass, leaving parent attributes unset.
  • Treating __private as a security control — name mangling only prevents accidental collisions.
  • Deep inheritance towers where composition, a mixin, or a plain function would be clearer.
  • Overriding __eq__ without __hash__, making instances unusable in sets and dict keys.
  • Doing network or file I/O inside __init__, making objects slow to build and impossible to test offline.
  • Leaking secrets through a default __repr__ into logs and tracebacks.

Best Practices

  • Keep __init__ cheap and side-effect free; do I/O in explicit methods.
  • Define __repr__ on every class you will debug — it makes logs and tracebacks readable.
  • Prefer composition ("has-a") over deep inheritance ("is-a") chains.
  • Use @dataclass for data-holder classes and abc.ABC to pin down plugin interfaces.
  • Initialise mutable state inside __init__ or with default_factory, never as a bare class attribute.
  • Call super().__init__(...) in every subclass constructor.
  • Favour @property over get_x() / set_x() for validated attribute access.

Review Questions

  1. What is the difference between a class attribute and an instance attribute, and how does attribute lookup resolve between them?
  2. When should you use a @classmethod rather than a @staticmethod?
  3. What security issue can occur if a mutable class attribute is used to collect scan results across multiple targets?
  4. Why is __private not a security control, and what should you use instead for real secrets?
  5. How would you troubleshoot an AttributeError raised inside an inherited method?
  6. What is the difference between __str__ and __repr__, and which should you define first?
  7. Write a Python example that demonstrates polymorphic dispatch across two classes with no shared base class.

Commands

# Run the scanner skeleton from this page
python3 scanner_skeleton.py

# Inspect a class interactively
python3 -c "import ipaddress; print(type(ipaddress.ip_address('127.0.0.1')).__mro__)"

# List the attributes and methods an object exposes
python3 -c "from pathlib import Path; print([n for n in dir(Path('.')) if not n.startswith('_')])"

# Show a class's method resolution order
python3 -c "from ipaddress import IPv4Address; print(IPv4Address.__mro__)"

References

Related Notes

  • [[Methods-and-Functions/Readme|Methods & Functions]] — functions vs methods, the building blocks of classes
  • [[Error-and-Exception-Handling/Readme|Error & Exception Handling]] — raising and handling errors in constructors and setters
  • [[Advanced-Python-Data-Structures/Readme|Advanced Python Data Structures]] — dataclasses, properties, generators
  • [[Advanced-Python-Modules/Readme|Advanced Python Modules]] — collections, abc, and other stdlib modules used here
  • [[Security-Tool-Development/Readme|Security Tool Development]] — applying these classes to real tooling
  • [[Readme|Python for Security Professionals]] — course home

Navigation

  • Previous: [[Methods-and-Functions/Readme|5. Methods & Functions]]
  • Home: [[Readme|Course Home]]
  • Next: [[Input-Output-File-Handling/Readme|7. Input/Output File Handling]]