Skip to content

add --update-db option to update local MAC address prefix database#413

Merged
kimocoder merged 3 commits into
kimocoder:masterfrom
JustGuardian:master
Oct 22, 2025
Merged

add --update-db option to update local MAC address prefix database#413
kimocoder merged 3 commits into
kimocoder:masterfrom
JustGuardian:master

Conversation

@JustGuardian

@JustGuardian JustGuardian commented Oct 14, 2025

Copy link
Copy Markdown

Created --update-db and added a python version of tools/fetch-oui.
I dont mind if you change any of the names that I have assigned (like --update-db or the class dbupdater.py)

@sourcery-ai

sourcery-ai Bot commented Oct 14, 2025

Copy link
Copy Markdown

Reviewer's Guide

Introduce a new --update-db option and Python-based database updater to fetch and refresh the local MAC address prefix file from IEEE registries, plus a standalone script duplicating that functionality.

Sequence diagram for --update-db option triggering database update

sequenceDiagram
actor User
participant "wifite.py"
participant "Configuration"
participant "DBUpdater"
User->>"wifite.py": Run with --update-db
"wifite.py"->>"Configuration": load_from_arguments()
"wifite.py"->>"DBUpdater": run()
"DBUpdater"->>"Configuration": initialize(False)
"DBUpdater"->>"DBUpdater": is_up_to_date(filename)
alt Database is up to date
"DBUpdater"-->>"wifite.py": Print up-to-date message
else Database needs update
"DBUpdater"->>"DBUpdater": update_all(filename, verbose)
"DBUpdater"-->>"wifite.py": Print completion message
end
Loading

Class diagram for new DBUpdater class and Configuration changes

classDiagram
class Configuration {
  +update_db: bool
  +db_filename: str
  +initialize(load_interface: bool)
  +load_from_arguments()
  +validate()
}
class DBUpdater {
  +SOURCES: dict
  +DEFAULT_FILENAME: str
  +run()
  +update_all(filename: str, verbose: bool): int
  +fetch_csv(url: str, verbose: bool): str
  +parse_and_write_csv(csv_content: str, outfile, key: str, verbose: bool): int
  +is_up_to_date(filename: str): (bool, str)
}
Configuration <.. DBUpdater : uses
Loading

File-Level Changes

Change Details Files
Add update_db flag and configuration attributes
  • Declare update_db and db_filename in Configuration
  • Set defaults (False and 'ieee-oui.txt') in initialize()
  • Load update_db from CLI args in load_from_arguments()
wifite/config.py
Integrate DBUpdater into CLI flow
  • Register --update-db in args parser
  • Import DBUpdater and branch to DBUpdater.run() when flag is set
wifite/args.py
wifite/wifite.py
Implement DBUpdater utility to download and parse IEEE registries
  • Define SOURCES and DEFAULT_FILENAME
  • Check existing file age and skip if up-to-date
  • Fetch CSVs, parse records, and write MAC Vendor lines
  • Handle interruptions and errors without stopping other sources
wifite/util/dbupdater.py
Add standalone fetch_oui.py tool
  • Provide a script with CLI flags for output file and verbose
  • Download and parse the same IEEE registries to text format
  • Remove existing file before writing new data
tools/fetch_oui.py

Possibly linked issues

  • #the issue: The PR implements the 'fetch-oui' tool in Python and adds an '--update-db' switch, directly addressing the issue's todo item.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and they look great!

Blocking issues:

  • Detected a 'requests' call without a timeout set. By default, 'requests' calls wait until the connection is closed. This means a 'requests' call without a timeout will hang the program if a response is never received. Consider setting a timeout for all 'requests'. (link)
Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `wifite/util/dbupdater.py:34` </location>
<code_context>
+    @classmethod
+    def run(cls):
+        
+        Configuration.initialize(False)
+
+        filename = Configuration.db_filename
</code_context>

<issue_to_address>
**question (bug_risk):** Re-initializing Configuration may override user-supplied values.

Review whether Configuration.initialize(False) is needed here, as it may reset db_filename or verbose set by the caller. Preserving existing values may be preferable.
</issue_to_address>

### Comment 2
<location> `wifite/util/dbupdater.py:83-91` </location>
<code_context>
+        headers = {"User-Agent": "Mozilla/5.0 (compatible; DBUpdater/1.0)"}
+        if verbose:
+            Color.pl('  →  Fetching %s' % url)
+        response = requests.get(url, headers=headers, timeout=30)
+        if not response.ok:
+            raise RuntimeError(f"Failed to fetch {url}: {response.status_code} {response.reason}")
</code_context>

<issue_to_address>
**suggestion:** No retry logic for transient network failures.

Consider implementing retries or handling transient exceptions to prevent updates from failing due to temporary network issues.

```suggestion
        import time
        from requests.exceptions import ConnectionError, Timeout, RequestException

        headers = {"User-Agent": "Mozilla/5.0 (compatible; DBUpdater/1.0)"}
        max_retries = 3
        backoff = 5  # seconds

        for attempt in range(1, max_retries + 1):
            if verbose:
                Color.pl(f'  →  Fetching {url} (attempt {attempt}/{max_retries})')
            try:
                response = requests.get(url, headers=headers, timeout=30)
                if not response.ok:
                    raise RuntimeError(f"Failed to fetch {url}: {response.status_code} {response.reason}")
                if len(response.content) == 0:
                    raise RuntimeError(f"Empty response from {url}")
                return response.text
            except (ConnectionError, Timeout) as e:
                if attempt == max_retries:
                    raise RuntimeError(f"Failed to fetch {url} after {max_retries} attempts: {e}")
                if verbose:
                    Color.pl(f"  →  Network error: {e}. Retrying in {backoff} seconds...")
                time.sleep(backoff)
            except RequestException as e:
                raise RuntimeError(f"Failed to fetch {url}: {e}")
```
</issue_to_address>

### Comment 3
<location> `wifite/util/dbupdater.py:41` </location>
<code_context>
+    verbose = args.v
+
+    # Delete old file if exists
+    if os.path.exists(filename):
+        if verbose:
+            print(f"Deleting existing {filename}")
</code_context>

<issue_to_address>
**issue (bug_risk):** Deleting the existing database file before confirming successful download may risk data loss.

If the update fails post-deletion, the local database is unrecoverable. Write to a temp file and replace the original only after a successful update to prevent this.
</issue_to_address>

### Comment 4
<location> `wifite/util/dbupdater.py:17` </location>
<code_context>
+# TODO: Add logging support, dry-run, and a --no-download option for offline parsing
+
+
+class DBUpdater:
+    """Updates a local database of MAC address prefixes to vendor names from IEEE registries.
+    """
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the code to use module-level functions instead of a class to simplify structure and clarify data flow.

Here’s one way to flatten the abstraction by turning all of your `@classmethod`s into module-level functions.  You keep 100% of the behavior, but drop the unnecessary `DBUpdater` wrapper (and its boilerplate).

1) Pull your constants out to module‐scope:
```python
SOURCES = {
    "OUI":   "https://standards-oui.ieee.org/oui/oui.csv",
    #
}
DEFAULT_FILENAME = "ieee-oui.txt"
```

2) Turn each `@classmethod` into a top-level function:
```python
def is_up_to_date(filename: str) -> (bool, str):
    mtime = os.path.getmtime(filename)
    last = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
    age  = datetime.now().timestamp() - mtime
    return (age < 7 * 24 * 3600), last

def fetch_csv(url: str, verbose: bool = False) -> str:
    headers = {"User-Agent": "DBUpdater/1.0"}
    if verbose:
        Color.pl(f"  →  Fetching {url}")
    r = requests.get(url, headers=headers, timeout=30)
    if not r.ok or not r.content:
        raise RuntimeError(f"Failed to fetch {url}: {r.status_code} {r.reason}")
    return r.text

def parse_and_write_csv(txt: str, out, key: str, verbose: bool = False) -> int:
    reader = csv.DictReader(txt.splitlines())
    out.write(f"\n# Start of {key}\n")
    cnt = 0
    for row in reader:
        mac    = row.get("Assignment") or row.get("Registry") or ""
        vendor = (row.get("Organization Name") or row.get("Organization") or "").strip()
        if mac and vendor:
            out.write(f"{mac}\t{vendor}\n")
            cnt += 1
    out.write(f"# End of {key}. {cnt} entries.\n")
    Color.p(f"   Wrote {cnt} entries from {key}")
    return cnt
```

3) Rewrite your main loop as a single `run()`:
```python
def update_all(filename: str, verbose: bool) -> int:
    total = 0
    with open(filename, "w", encoding="utf-8") as out:
        out.write(f"# Generated {datetime.now():%Y-%m-%d %H:%M:%S}\n")
        for key, url in SOURCES.items():
            Color.pl(f"\n{{+}} Processing {key} from {url}")
            try:
                txt = fetch_csv(url, verbose)
                total += parse_and_write_csv(txt, out, key, verbose)
            except Exception as e:
                print(f"Error processing {key}: {e}", file=sys.stderr)
    return total

def run():
    Configuration.initialize(False)
    fn, vb = Configuration.db_filename, bool(Configuration.verbose)
    if os.path.exists(fn):
        up, last = is_up_to_date(fn)
        if up:
            return Color.pl(f"Database up-to-date ({last})")
        if vb:
            Color.pl(f"Deleting old {fn}")
        os.remove(fn)
    try:
        cnt = update_all(fn, vb)
    except KeyboardInterrupt:
        return Color.pl("Interrupted by user")
    Color.pl(f"Done – total entries: {cnt}")

if __name__ == "__main__":
    run()
```

This removes one level of nesting, clarifies data flow, and avoids a “no-state” class.
</issue_to_address>

### Comment 5
<location> `tools/fetch_oui.py:25` </location>
<code_context>
    response = requests.get(url, headers=headers)
</code_context>

<issue_to_address>
**security (python.requests.best-practice.use-timeout):** Detected a 'requests' call without a timeout set. By default, 'requests' calls wait until the connection is closed. This means a 'requests' call without a timeout will hang the program if a response is never received. Consider setting a timeout for all 'requests'.

```suggestion
    response = requests.get(url, headers=headers, timeout=30)
```

*Source: opengrep*
</issue_to_address>

### Comment 6
<location> `wifite/util/dbupdater.py:85` </location>
<code_context>
    @classmethod
    def fetch_csv(cls, url: str, verbose: bool = False) -> str:
        """Download CSV content (boilerplate; uses requests)."""
        headers = {"User-Agent": "Mozilla/5.0 (compatible; DBUpdater/1.0)"}
        if verbose:
            Color.pl('  →  Fetching %s' % url)
        response = requests.get(url, headers=headers, timeout=30)
        if not response.ok:
            raise RuntimeError(f"Failed to fetch {url}: {response.status_code} {response.reason}")
        if len(response.content) == 0:
            raise RuntimeError(f"Empty response from {url}")
        return response.text

</code_context>

<issue_to_address>
**suggestion (code-quality):** Replace interpolated string formatting with f-string ([`replace-interpolation-with-fstring`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/replace-interpolation-with-fstring/))

```suggestion
            Color.pl(f'  →  Fetching {url}')
```
</issue_to_address>

### Comment 7
<location> `wifite/util/dbupdater.py:111-116` </location>
<code_context>
    def is_up_to_date(filename: str) -> bool:

        mtime = os.path.getmtime(filename)
        last_update = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
        age_seconds = datetime.now().timestamp() - mtime
        return age_seconds < (7 * 24 * 3600), last_update #if file is older than 7 days it is not up to date

</code_context>

<issue_to_address>
**issue (code-quality):** The first argument to instance methods should be `self` ([`instance-method-first-arg-name`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/instance-method-first-arg-name/))
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread wifite/util/dbupdater.py
Comment thread wifite/util/dbupdater.py Outdated
Comment thread wifite/util/dbupdater.py
Comment thread tools/fetch_oui.py Outdated
Comment thread wifite/util/dbupdater.py
Comment thread wifite/util/dbupdater.py
JustGuardian and others added 2 commits October 14, 2025 22:34
Added timeout

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
@kimocoder kimocoder merged commit fd8ec3d into kimocoder:master Oct 22, 2025
11 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants