diff --git a/README.md b/README.md index f360b3f..58c9c40 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,101 @@ -# GitHub Codespaces ♥️ React - -Welcome to your shiny new Codespace running React! We've got everything fired up and running for you to explore React. - -You've got a blank canvas to work on from a git perspective as well. There's a single initial commit with the what you're seeing right now - where you go from here is up to you! - -Everything you do here is contained within this one codespace. There is no repository on GitHub yet. If and when you’re ready you can click "Publish Branch" and we’ll create your repository and push up your project. If you were just exploring then and have no further need for this code then you can simply delete your codespace and it's gone forever. - -This project was bootstrapped for you with [Vite](https://vitejs.dev/). - -## Available Scripts - -In the project directory, you can run: - -### `npm start` - -We've already run this for you in the `Codespaces: server` terminal window below. If you need to stop the server for any reason you can just run `npm start` again to bring it back online. - -Runs the app in the development mode.\ -Open [http://localhost:3000/](http://localhost:3000/) in the built-in Simple Browser (`Cmd/Ctrl + Shift + P > Simple Browser: Show`) to view your running application. - -The page will reload automatically when you make changes.\ -You may also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `npm run build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -## Learn More - -You can learn more in the [Vite documentation](https://vitejs.dev/guide/). - -To learn Vitest, a Vite-native testing framework, go to [Vitest documentation](https://vitest.dev/guide/) - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: [https://sambitsahoo.com/blog/vite-code-splitting-that-works.html](https://sambitsahoo.com/blog/vite-code-splitting-that-works.html) - -### Analyzing the Bundle Size - -This section has moved here: [https://github.com/btd/rollup-plugin-visualizer#rollup-plugin-visualizer](https://github.com/btd/rollup-plugin-visualizer#rollup-plugin-visualizer) - -### Making a Progressive Web App - -This section has moved here: [https://dev.to/hamdankhan364/simplifying-progressive-web-app-pwa-development-with-vite-a-beginners-guide-38cf](https://dev.to/hamdankhan364/simplifying-progressive-web-app-pwa-development-with-vite-a-beginners-guide-38cf) - -### Advanced Configuration - -This section has moved here: [https://vitejs.dev/guide/build.html#advanced-base-options](https://vitejs.dev/guide/build.html#advanced-base-options) - -### Deployment - -This section has moved here: [https://vitejs.dev/guide/build.html](https://vitejs.dev/guide/build.html) - -### Troubleshooting - -This section has moved here: [https://vitejs.dev/guide/troubleshooting.html](https://vitejs.dev/guide/troubleshooting.html) +# Facebook Security Analyzer CLI + +This command-line tool provides utilities to help users analyze Facebook messages for potential phishing attempts and assess Facebook profiles for indicators of fakeness. + +## Features + +1. **Phishing Message Analysis:** + * Accepts user-provided message text. + * Scans for common phishing keywords (e.g., "verify your account," "urgent security alert"). + * Extracts URLs and checks them against patterns of suspicious URLs (e.g., impersonation of legitimate domains, use of URL shorteners, IP address links). + * Provides a "suspicion score" and a summary of findings. + +2. **Fake Profile Analysis (Manual Check):** + * Accepts a Facebook profile URL from the user. + * **Important:** This tool *does not* scrape Facebook or automatically access profile data, in compliance with Facebook's policies. + * It guides the user through a manual checklist of common fake profile indicators (e.g., generic profile picture, recent account age, low activity, poor grammar, suspicious requests). + * Includes an interactive helper to guide the user in performing a reverse image search on the profile picture using external services like Google Images or TinEye. + * Calculates a "suspicion score" based on the user's answers and provides an assessment of how likely the profile is to be fake. + +## Prerequisites + +* Python 3.x + +## Installation & Setup + +1. **Clone the repository or download the files.** + If you have the files (`main.py` and the `facebook_analyzer` directory) in a single project folder, no complex installation is typically needed. + +2. **No external Python libraries are required** for the core functionality as it currently stands (uses only standard libraries like `re` and `webbrowser`). If future enhancements (like direct API calls for URL checking) are added, this section will need updates. + +## How to Run + +1. Open your terminal or command prompt. +2. Navigate to the directory where you saved the `main.py` file and the `facebook_analyzer` folder. +3. Run the application using the Python interpreter: + ```bash + python main.py + ``` + +4. The tool will display a menu: + + ``` + --- Facebook Security Analyzer --- + Choose an option: + 1. Analyze a message for phishing + 2. Analyze a Facebook profile for fakeness (manual check) + 3. Exit + ------------------------------------ + Enter your choice (1-3): + ``` + +## Usage + +### 1. Analyze a message for phishing + +* Select option `1`. +* When prompted, paste the full text of the suspicious message and press Enter. +* The tool will output: + * A phishing likelihood score (higher is more suspicious). + * Any suspicious keywords found. + * Any suspicious URLs found, along with the reason they were flagged. + * An overall summary. + +### 2. Analyze a Facebook profile for fakeness (manual check) + +* Select option `2`. +* When prompted, enter the full Facebook profile URL (e.g., `https://www.facebook.com/some.profile`). +* The tool will open the profile URL in your default web browser for your manual inspection. +* You will then be guided through a series of yes/no questions based on your observations of the profile. + * This includes an optional guided step to perform a reverse image search on the profile's picture. +* After you answer all questions, the tool will provide: + * A list of fake profile indicators you noted. + * An overall "suspicion score." + * An assessment category (e.g., Low, Medium, High likelihood of being fake). + +## Disclaimer + +* This tool provides heuristic-based analysis and guidance. It is **not foolproof** and should not be considered a definitive judgment on whether a message is phishing or a profile is fake. +* **Phishing Detection:** The tool uses a predefined list of keywords and URL patterns. Sophisticated phishing attempts may evade these checks. Always exercise extreme caution with suspicious messages, especially those asking for login credentials or personal information. Do not rely solely on this tool. +* **Fake Profile Detection:** The analysis is based *entirely* on your manual observations and answers. False positives and negatives are possible. Always use your best judgment when interacting with profiles online. +* **Facebook's Terms of Service:** This tool is designed to operate without violating Facebook's Terms of Service by not scraping or automatically collecting data from its platform. The fake profile analysis relies on user-driven manual checks. +* **Reporting:** If you encounter a phishing attempt or a malicious fake profile, report it directly to Facebook through their official reporting channels. + +## File Structure +``` +. +├── main.py # Main CLI application script +├── facebook_analyzer/ +│ ├── __init__.py # Makes facebook_analyzer a Python package +│ ├── phishing_detector.py # Logic for phishing message analysis +│ └── fake_profile_detector.py # Logic for fake profile interactive checklist +└── README.md # This documentation file +``` + +## Future Enhancements (Potential) + +* Integration with external URL checking services/APIs (e.g., Google Safe Browsing) for more robust phishing detection. +* More sophisticated text analysis for phishing detection (e.g., NLP techniques). +* Allowing users to customize keyword lists. +* A graphical user interface (GUI) instead of a CLI. +``` diff --git a/README_iot_simulator.md b/README_iot_simulator.md new file mode 100644 index 0000000..f11a07d --- /dev/null +++ b/README_iot_simulator.md @@ -0,0 +1,138 @@ +# Generic IoT Data Simulator + +This Python CLI tool simulates various types of IoT sensor data and sends it to configurable endpoints (console or HTTP POST). It's designed to help developers test IoT backends, data ingestion pipelines, dashboards, and other components without needing physical hardware. + +## Features + +* **Multiple Sensor Types:** Simulate data for: + * Temperature (°C) + * Humidity (%) + * GPS Location (latitude, longitude - with random walk) + * Boolean Status (e.g., on/off, open/closed) + * Incrementing Counter +* **Configurable Device Profiles:** Define multiple simulated devices, each with its own set of sensors. Device IDs can be specified or auto-generated. +* **Flexible Output:** + * **Console:** Print generated data as structured JSON to the standard output. + * **HTTP POST:** Send data as a JSON payload to a specified HTTP(S) endpoint. +* **Customizable Simulation:** + * Control the interval between messages. + * Set the total number of messages to send (or run indefinitely). +* **Standardized Data Format:** Output includes `deviceId`, `timestamp` (ISO 8601 UTC), and sensor readings. +* **Stateful Sensors:** GPS coordinates evolve with a random walk, and counters increment per device. + +## Prerequisites + +* Python 3.6+ +* `requests` library (for HTTP output). + +## Installation + +1. **Clone the repository or download the files.** + Ensure you have `iot_sim_main.py`, the `iot_simulator` directory (containing `generators.py`, `publishers.py`, `__init__.py`), and the `iot_simulator_requirements.txt` file. + +2. **Create a virtual environment (recommended):** + ```bash + python3 -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` + +3. **Install dependencies:** + Navigate to the directory containing `iot_simulator_requirements.txt` and run: + ```bash + pip install -r iot_simulator_requirements.txt + ``` + +## Usage + +The tool is run from the command line using `python3 iot_sim_main.py`. + +### Command-Line Arguments + +* `-p, --profiles PROFILES [PROFILES ...]`: **(Required)** + * Defines one or more device profiles. + * Each profile can be a string in the format `"device_id:sensor1,sensor2,..."`. + * If `device_id:` is omitted (e.g., `"sensor1,sensor2"`), a unique device ID will be auto-generated. + * Alternatively, you can provide a single argument which is a JSON string representing a list of profiles: `'[{"id":"dev1","sensors":["temp","hum"]}, {"id":"dev2","sensors":["gps"]}]'` + * **Supported sensor types:** `temperature`, `humidity`, `gps`, `status`, `counter`. +* `-i, --interval INTERVAL`: (Optional) Interval in seconds between sending message batches (default: 5.0 seconds). +* `-n, --num_messages NUM_MESSAGES`: (Optional) Number of message batches to send. A batch includes data from all defined profiles. Set to `0` for an infinite simulation (stop with Ctrl+C) (default: 10). +* `-o, --output {console,http}`: (Optional) Output target. Defaults to `console`. +* `--http_url HTTP_URL`: (Optional) Target URL for HTTP POST output. **Required if `--output=http` is chosen.** +* `-h, --help`: Show help message and exit. + +### JSON Payload Format + +The data is sent/printed as a JSON object with the following structure: +```json +{ + "deviceId": "your_device_id", + "timestamp": "YYYY-MM-DDTHH:MM:SS.ffffffZ", // ISO 8601 UTC + // Sensor fields appear here based on profile + "temperature_celsius": 23.7, + "humidity_percent": 45.8, + "location": { + "latitude": 34.052200, + "longitude": -118.243700 + }, + "active_status": true, + "event_count": 123 +} +``` +*Note: Not all sensor fields will be present in every message; only those configured for the specific device profile.* + +### Examples + +1. **Simulate one device with temperature and humidity, output to console (10 messages, 5s interval):** + ```bash + python3 iot_sim_main.py --profiles "device001:temperature,humidity" + ``` + +2. **Simulate two devices, 5 messages, 2s interval, output to console:** + ```bash + python3 iot_sim_main.py \ + --profiles "thermostat1:temperature" "gps_tracker:gps,counter" \ + --num_messages 5 \ + --interval 2 + ``` + +3. **Simulate one device (auto-generated ID) with all sensor types, run indefinitely, output to console every 10s:** + ```bash + python3 iot_sim_main.py \ + --profiles "temperature,humidity,gps,status,counter" \ + --num_messages 0 \ + --interval 10 + ``` + +4. **Simulate devices defined in a JSON string, output to an HTTP endpoint:** + ```bash + python3 iot_sim_main.py \ + --profiles '[{"id":"factory_sensor_A","sensors":["temperature","counter"]},{"id":"asset_tracker_B","sensors":["gps"]}]' \ + --output http \ + --http_url "http://localhost:8080/api/data" \ + --interval 2 \ + --num_messages 100 + ``` + *(Ensure you have an HTTP server listening at the specified `--http_url` if testing HTTP output.)* + +## File Structure +``` +. +├── iot_simulator/ +│ ├── __init__.py # Makes 'iot_simulator' a Python package +│ ├── generators.py # Logic for generating sensor data +│ └── publishers.py # Logic for formatting and sending data (console, HTTP) +├── iot_sim_main.py # CLI entry point and main simulation loop +├── iot_simulator_requirements.txt # Python dependencies (requests) +└── README_iot_simulator.md # This documentation file +``` + +## Sensor Details + +* **temperature:** Random float, default range -10.0 to 40.0 °C. Output key: `temperature_celsius`. +* **humidity:** Random float, default range 20.0 to 80.0 %. Output key: `humidity_percent`. +* **gps:** Simulates GPS coordinates (latitude, longitude) starting from a base point and performing a small random walk with each update. Output key: `location` (an object with `latitude` and `longitude`). +* **status:** Random boolean (True/False). Output key: `active_status`. +* **counter:** Integer that increments by 1 for each message from that specific device. Output key: `event_count`. + +Sensor data generation (ranges, GPS step) is defined in `iot_simulator/generators.py` and can be customized there if needed. +``` diff --git a/README_netmap.md b/README_netmap.md new file mode 100644 index 0000000..689ad6c --- /dev/null +++ b/README_netmap.md @@ -0,0 +1,118 @@ +# Network Infrastructure Mapping Tool (NetMap CLI) + +NetMap CLI is a Python tool that generates network topology diagrams and graph data files from a simple CSV input. It's designed to quickly visualize device connectivity. + +## Features + +* **CSV Input:** Reads network links from a CSV file with 'SourceDevice' and 'TargetDevice' columns. +* **Graph Visualization:** Generates a PNG image of the network map using `matplotlib` and `networkx`. + * Supports multiple layout algorithms: `spring` (default), `kamada_kawai`, `circular`, `random`, `shell`, `spectral`. +* **Graph Data Export:** Can export the network graph to: + * GEXF format (for use in Gephi or other graph analysis tools). + * GraphML format (another common format for graph tools). +* **Command-Line Interface:** Easy-to-use CLI for specifying input and output options. + +## Prerequisites + +* Python 3.6+ +* Libraries: `networkx`, `matplotlib`, `scipy` (SciPy is a dependency for some `networkx` layout algorithms). + +## Installation + +1. **Clone the repository or download the files.** + Ensure you have the `netmap_tool` directory (containing `main.py`, `mapper.py`, `__init__.py`) and the `requirements.txt` file. + +2. **Create a virtual environment (recommended):** + ```bash + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` + +3. **Install dependencies:** + Navigate to the directory containing `requirements.txt` and run: + ```bash + pip install -r requirements.txt + ``` + +## Usage + +The tool is run from the command line using `python -m netmap_tool.main`. + +### Command-Line Arguments + +* `--input_csv INPUT_CSV`: **(Required)** Path to the input CSV file. + * The CSV file must contain 'SourceDevice' and 'TargetDevice' headers. + * Each row defines a link between the two specified devices. +* `--output_png OUTPUT_PNG`: (Optional) Path to save the output PNG image (e.g., `mymap.png`). +* `--layout {spring,kamada_kawai,circular,random,shell,spectral}`: (Optional) Layout algorithm for the PNG map. Defaults to `spring`. +* `--output_gexf OUTPUT_GEXF`: (Optional) Path to save the graph in GEXF format (e.g., `mymap.gexf`). +* `--output_graphml OUTPUT_GRAPHML`: (Optional) Path to save the graph in GraphML format (e.g., `mymap.graphml`). +* `-h, --help`: Show help message and exit. + +### Examples + +1. **Generate a PNG map with the default spring layout:** + ```bash + python -m netmap_tool.main --input_csv path/to/your/links.csv --output_png network_diagram.png + ``` + +2. **Generate a PNG map with the Kamada-Kawai layout:** + ```bash + python -m netmap_tool.main --input_csv links.csv --output_png map_kk.png --layout kamada_kawai + ``` + +3. **Export the graph to GEXF format:** + ```bash + python -m netmap_tool.main --input_csv links.csv --output_gexf my_network.gexf + ``` + +4. **Generate a PNG and also export to GraphML:** + ```bash + python -m netmap_tool.main --input_csv links.csv --output_png visual_map.png --output_graphml data_map.graphml + ``` + +5. **Only build the graph and print info (no file output):** + ```bash + python -m netmap_tool.main --input_csv links.csv + ``` + *(The tool will inform you that no output options were specified)* + +### CSV File Format + +Your input CSV file must have a header row with `SourceDevice` and `TargetDevice`. + +**Example `links.csv`:** +```csv +SourceDevice,TargetDevice +RouterA,Switch1 +RouterB,Switch1 +RouterA,RouterB +Firewall,RouterA +Switch1,ServerX +``` + +## File Structure +``` +. +├── netmap_tool/ +│ ├── __init__.py # Makes 'netmap_tool' a Python package +│ ├── main.py # CLI entry point +│ └── mapper.py # Core logic for graph building, drawing, and exporting +├── requirements.txt # Python dependencies +└── README_netmap.md # This documentation file +``` + +## Limitations + +* **Simplicity:** This tool is designed for simple, direct device-to-device connectivity visualization. It does not parse complex configurations or infer relationships beyond what's explicitly in the CSV. +* **Layouts:** Graph layout can be challenging for large or complex networks. The default layouts are good for small to medium-sized graphs. For very large networks, using the GEXF/GraphML export with a dedicated tool like Gephi is recommended for better layout control and exploration. +* **No Device Details:** The map does not include interface names, IP addresses, VLANs, or other detailed device information. It only shows device names and their connections. +* **Error Handling:** Basic error handling for file operations and CSV parsing is included. More complex CSV issues might require manual data cleaning. + +## Future Enhancements (Potential) + +* Support for other input formats (e.g., JSON, YAML). +* Ability to add attributes to nodes/edges via CSV (e.g., device type, link speed) and visualize them. +* More sophisticated layout options or interactivity (would likely require different libraries e.g., web-based). +* Basic parsing of simplified device configuration snippets (e.g., `show cdp neighbor` output). +``` diff --git a/README_scam_detector.md b/README_scam_detector.md new file mode 100644 index 0000000..2aba21c --- /dev/null +++ b/README_scam_detector.md @@ -0,0 +1,104 @@ +# Text-Based Scam Detection Tool + +This Python CLI tool analyzes text content to identify common indicators associated with scams, such as phishing, prize scams, tech support scams, and more. It provides a likelihood score and a list of specific heuristics triggered. + +## Features + +* **Heuristic-Based Analysis:** Checks text against predefined lists of keywords and regular expression patterns related to: + * Urgency and pressure tactics. + * Requests for sensitive information (credentials, financial details). + * Too-good-to-be-true offers and prizes. + * Generic greetings. + * Tech support scam language. + * Payment requests and mentions of cryptocurrencies/gift cards. +* **URL Analysis (String-Based):** + * Detects URLs within the text. + * Checks for suspicious Top-Level Domains (TLDs). + * Looks for suspicious keywords (e.g., "login", "verify") in URL paths or domains (with a basic check to avoid flagging known major domains). +* **Detection of Potential Identifiers:** + * Cryptocurrency addresses (BTC, ETH). + * Phone numbers (basic detection). +* **Scam Likelihood Score:** Calculates a score based on the number and severity (weights) of indicators found. +* **Configurable Input:** Accepts text via direct command-line argument, from a file, or from standard input (stdin). +* **Verbose Output:** Option to display detailed analysis of URLs found. +* **Adjustable Threshold:** Set a score threshold for a "High Risk" warning. + +## Prerequisites + +* Python 3.6+ +* No external Python libraries are required for the core functionality (uses only standard libraries like `re`, `argparse`, `urllib.parse`). + +## Installation + +1. **Download the Code:** + * Ensure you have `scam_main.py` and the `scam_detector` directory (containing `analyzer.py`, `heuristics.py`, `__init__.py`). + +2. **No `pip install` needed for external libraries for the core tool.** + +## Usage + +The tool is run from the command line using `python3 scam_main.py`. You must provide one input method. + +### Command-Line Arguments + +* **Input (Required - choose one):** + * `-t TEXT, --text TEXT`: Text content to analyze directly. + * `-f FILE, --file FILE`: Path to a plain text file to read content from. + * `--stdin`: Read text content from standard input (e.g., via a pipe). +* **Options:** + * `-v, --verbose`: Enable verbose output (shows detailed URL analysis if URLs are found). + * `--threshold THRESHOLD`: Score threshold above which a 'High Risk' warning is displayed (default: 5.0). + * `-h, --help`: Show help message and exit. + +### Examples + +1. **Analyze text directly:** + ```bash + python3 scam_main.py --text "Dear Customer, your account is suspended. Please login at http://yourbank.suspicious-site.xyz/update to avoid closure." + ``` + +2. **Analyze text from a file:** + ```bash + python3 scam_main.py --file path/to/suspicious_email.txt + ``` + +3. **Analyze text from a file with verbose output and a custom threshold:** + ```bash + python3 scam_main.py --file message.txt --verbose --threshold 3.0 + ``` + +4. **Analyze text piped from another command (Linux/macOS):** + ```bash + cat email_body.txt | python3 scam_main.py --stdin + ``` + *(On Windows, you might type input then Ctrl+Z, Enter for stdin)* + +## Interpreting the Output + +* **Overall Scam Likelihood Score:** A numerical score. Higher scores indicate a higher likelihood of the text being a scam based on the tool's heuristics. +* **Assessment:** A qualitative assessment (e.g., "Low risk," "Medium risk," "WARNING: High risk!") based on the score and the threshold. +* **Indicators Found:** A list of specific reasons why the text was flagged (e.g., presence of urgency keywords, suspicious URL TLDs). +* **Detailed URL Analysis (with `--verbose`):** For each URL found: + * The URL string. + * Whether it was deemed suspicious. + * Specific reasons for suspicion (e.g., "Uses a potentially suspicious TLD," "URL contains suspicious keyword"). + +## Disclaimer + +* **Heuristic-Based, Not Foolproof:** This tool uses a set of predefined rules, keywords, and patterns. It is **not a definitive judgment** on whether a piece of text is a scam. Scammers constantly evolve their tactics. +* **False Positives/Negatives:** The tool may incorrectly flag legitimate text as suspicious (false positive) or fail to detect a real scam (false negative). +* **Context is Key:** The tool does not understand the full context of the communication, the sender, or your relationship with them, all of which are crucial for accurately identifying scams. +* **Use Your Judgment:** **Always exercise extreme caution and use your best judgment** when dealing with unsolicited communications, requests for personal information, or offers that seem too good to be true. +* **Do Not Rely Solely on This Tool:** This tool is an aid and should be one of many factors in your decision-making process. If you are unsure about a message, consult trusted sources or individuals. + +## File Structure +``` +. +├── scam_detector/ +│ ├── __init__.py # Makes 'scam_detector' a Python package +│ ├── analyzer.py # Core scam analysis logic +│ └── heuristics.py # Keyword lists, regex patterns, and weights +└── scam_main.py # CLI entry point +└── README_scam_detector.md # This documentation file +``` +``` diff --git a/README_simple_browser.md b/README_simple_browser.md new file mode 100644 index 0000000..003bd6d --- /dev/null +++ b/README_simple_browser.md @@ -0,0 +1,101 @@ +# Simple Browser - Distraction-Free Web Viewer + +Simple Browser is a minimalist web browser built with Python and PyQt5. It's designed for users who need a distraction-free environment for focused reading, research, or browsing. The emphasis is on maximizing content display and minimizing UI clutter. + +## Features (MVP) + +* **Minimalist Interface:** A clean and simple UI with only essential controls visible. +* **URL Navigation:** Enter a web address in the URL bar to navigate. +* **Core Navigation Controls:** + * Back + * Forward + * Reload + * Home (navigates to a simple, clean start page) +* **Content-Focused Display:** Web pages are rendered by the QWebEngineView (based on Chromium's engine), aiming for good compatibility with modern web standards (HTML, CSS, basic JavaScript). +* **Dynamic Window Title:** The browser window title updates to reflect the title of the current webpage. +* **Basic Pop-up Control:** Attempts to suppress pop-up windows opened by JavaScript. +* **Plugin-Free:** Common browser plugins are disabled to maintain a lean environment. +* **Basic Error Page:** Displays a simple error message if a page fails to load. + +## Prerequisites + +* **Python 3.x** +* **PyQt5 and PyQtWebEngine:** These Python bindings for the Qt framework are essential. + +## Installation + +1. **Ensure Python 3 is installed.** + +2. **Install PyQt5 and PyQtWebEngine:** + Open your terminal or command prompt and run: + ```bash + pip install PyQt5 PyQtWebEngine + ``` + * **Note for Linux users:** On some Linux distributions, you might need to install additional Qt development packages if `pip` doesn't fully set them up. For example, on Debian/Ubuntu based systems: + ```bash + sudo apt-get install python3-pyqt5.qtwebengine + ``` + However, try the `pip install` command first as it often works directly. + * **Note for Windows/macOS users:** `pip` usually installs pre-compiled binaries that include the necessary Qt components. + +3. **Download the Code:** + * Place the `simple_browser` directory (containing `browser.py` and `__init__.py`) into your desired project location. + +## How to Run + +1. Open your terminal or command prompt. +2. Navigate to the directory **containing** the `simple_browser` folder. For example, if `simple_browser` is in `/path/to/project/`, you should be in `/path/to/project/`. +3. Run the browser application using: + ```bash + python -m simple_browser.browser + ``` + Alternatively, if your `simple_browser` directory contains a `main.py` or similar entry script that imports and runs the browser, you would run that. (The current structure assumes direct execution of `browser.py` as the main script). + + If you are inside the `simple_browser` directory itself, you can run: + ```bash + python browser.py + ``` + +## Usage + +* The browser window will open, displaying a simple start page. +* **URL Bar:** Type or paste a web address into the text field at the top and press `Enter` to navigate. +* **Buttons:** + * `< Back`: Navigates to the previous page in your history. + * `Forward >`: Navigates to the next page in your history (if you've gone back). + * `Reload`: Refreshes the current page. + * `Home`: Returns to the initial simple start page. + +## Known Limitations & Considerations + +* **Highly Simplified:** This browser intentionally lacks many features found in modern commercial browsers, such as: + * Bookmarks + * Detailed history management + * Extensions / Add-ons + * Advanced developer tools + * Complex settings and preferences + * Downloads management (files linked for download might behave unpredictably or try to open externally depending on OS and QtWebEngine defaults). +* **JavaScript Pop-ups:** While `JavascriptCanOpenWindows` is set to `false`, very persistent or creatively coded JavaScript might still find ways to show overlays or alerts within the page. True immunity is hard. +* **Ad Blocking:** No built-in ad-blocking is implemented in this MVP. +* **Security:** QWebEngine is based on Chromium and receives security updates, but a custom browser application built on top of it is only as secure as its weakest link and the diligence of its developer. This PoC is not hardened for use in high-risk environments without further security auditing. +* **Single Process (Typically):** QtWebEngine may use multiple processes under the hood for rendering, but the Python application itself is single-threaded unless explicitly coded otherwise. Very heavy pages could make the UI feel sluggish during load. + +## File Structure +``` +your_project_directory/ +└── simple_browser/ + ├── __init__.py # Makes 'simple_browser' a Python package + └── browser.py # The main application script for the browser +└── README_simple_browser.md # This documentation file +``` + +## Future Enhancements (Potential) + +* User-configurable home page. +* Basic bookmarking system. +* Tabbed browsing (would increase complexity). +* A dedicated full-screen/content-only mode that hides all UI chrome. +* More robust ad/tracker blocking via request interception. +* Customizable blocklists. +* Basic history accessible via a simple dropdown. +``` diff --git a/README_wifi_analyzer.md b/README_wifi_analyzer.md new file mode 100644 index 0000000..f87e698 --- /dev/null +++ b/README_wifi_analyzer.md @@ -0,0 +1,109 @@ +# Wi-Fi Analyzer CLI (Linux) + +This Python-based command-line tool helps you analyze your Wi-Fi environment on Linux systems. It can scan for nearby networks, show channel usage, and monitor the signal strength of your current connection. + +## Features + +* **Wi-Fi Network Scan:** + * Lists available Wi-Fi networks (SSIDs). + * Displays signal strength (quality percentage), channel, frequency (band), and security protocols. +* **Channel Usage Analysis:** + * Shows how many networks are operating on each channel in both 2.4 GHz and 5 GHz bands. + * Includes a simple text-based bar graph for 2.4 GHz channel congestion. +* **Current Connection Monitoring:** + * Displays the signal strength of your currently connected Wi-Fi network. + * Can use `nmcli` (shows quality %) or `iw` (shows dBm). + * Updates periodically for a specified duration. +* **Auto-detection of Wi-Fi Interface:** Attempts to find your Wi-Fi interface automatically. + +## Prerequisites + +* **Linux Operating System:** This tool is designed primarily for Linux and relies on Linux-specific command-line utilities. +* **Python 3.6+** +* **NetworkManager (`nmcli` command):** Required for most functionality. This is standard on most modern Linux desktop distributions. +* **Wireless Tools (`iw` command):** Required if you want to use the `--use_iw` option for signal monitoring (provides dBm values). Often available by default or can be installed (e.g., `sudo apt install iw`). +* **Wi-Fi Adapter:** A functional Wi-Fi adapter on your Linux machine. + +## Installation + +1. **Ensure Prerequisites are Met:** + * Verify `python3` is installed. + * Check if `nmcli` and `iw` are available by typing them in your terminal. If not, install them using your distribution's package manager (e.g., `sudo apt install network-manager iw`). + +2. **Download the Code:** + * Place the `wifi_analyzer` directory (containing `scanner.py`, `analyzer.py`, `monitor.py`, `__init__.py`) and the `wifi_main.py` script in your desired project location. + * No external Python libraries (from PyPI) are required by the script itself; it uses standard libraries and calls system utilities. + +## How to Run + +1. Open your terminal. +2. Navigate to the directory where you saved `wifi_main.py` and the `wifi_analyzer` folder. +3. Run the application using `python3`: + + ```bash + python3 wifi_main.py [options] + ``` + +## Usage & Options + +You must specify at least one action: `--scan`, `--channels`, or `--monitor`. + +* `-h, --help`: Show help message and exit. +* `--interface INTERFACE`: Specify the Wi-Fi interface (e.g., `wlan0`). If not provided, the tool will attempt to auto-detect it. +* `--scan`: Scan for available Wi-Fi networks and display their details. +* `--channels`: Display Wi-Fi channel usage analysis. This action automatically performs a scan if scan data isn't already available from a `--scan` action in the same command. +* `--monitor`: Monitor the signal strength of the current Wi-Fi connection. +* `--duration DURATION`: (Used with `--monitor`) Duration for monitoring in seconds (default: 10 seconds). +* `--interval INTERVAL`: (Used with `--monitor`) Interval for signal strength updates in seconds (default: 2 seconds). +* `--use_iw`: (Used with `--monitor`) Use the `iw` command for signal monitoring. This typically provides signal strength in dBm and might require `sudo` or specific permissions on some systems if `iw dev link` is restricted. `nmcli` (default) usually provides a quality percentage and often doesn't require `sudo` for this information. + +### Examples + +1. **Scan for Wi-Fi networks and show channel usage on auto-detected interface:** + ```bash + python3 wifi_main.py --scan --channels + ``` + +2. **Scan networks on a specific interface (`wlp2s0`):** + ```bash + python3 wifi_main.py --interface wlp2s0 --scan + ``` + +3. **Monitor current connection's signal strength for 30 seconds (using `nmcli`):** + ```bash + python3 wifi_main.py --monitor --duration 30 + ``` + +4. **Monitor current connection's signal strength using `iw` (dBm values):** + ```bash + python3 wifi_main.py --monitor --use_iw + ``` + +## Interpreting the Output + +* **Signal Strength:** + * `nmcli` (default for scan and monitor): Shows as a percentage (0-100%). Higher is better. + * `iw` (for `--monitor --use_iw`): Shows in dBm (e.g., -50 dBm). This is a negative value. Closer to 0 is better (e.g., -40 dBm is stronger than -70 dBm). +* **Channels (2.4 GHz Band):** Channels 1, 6, and 11 are generally recommended as they do not overlap. If you see many networks on other channels or heavily concentrated on one channel, it might indicate interference. +* **Security:** Prefer networks using WPA2 or WPA3. Avoid WEP or Open (unsecured) networks if possible. + +## Limitations + +* **Linux Only:** Relies on Linux-specific commands (`nmcli`, `iw`). It will not work on Windows or macOS without significant modifications. +* **`nmcli` / `iw` Availability:** These tools must be installed and accessible in the system's PATH. +* **Permissions:** While basic `nmcli dev wifi list` often works without `sudo`, `nmcli dev wifi rescan` (used by the script) or `iw dev scan` (an alternative not currently used as primary) might require `sudo` on some systems for full functionality or to get the freshest results. The script attempts a rescan; if it fails due to permissions, it may proceed with cached scan data. Monitoring with `iw` might also have permission sensitivities. +* **Accuracy:** Signal strength values can fluctuate and are dependent on your hardware and environment. +* **Parsing Robustness:** The parsing of `nmcli` and `iw` output is based on common formats but might break if the output format of these tools changes significantly in future versions or on highly customized systems. + +## File Structure +``` +. +├── wifi_analyzer/ +│ ├── __init__.py # Makes 'wifi_analyzer' a Python package +│ ├── scanner.py # Logic for scanning networks and parsing nmcli output +│ ├── analyzer.py # Logic for channel usage analysis +│ └── monitor.py # Logic for monitoring current connection signal +└── wifi_main.py # CLI entry point script +└── README_wifi_analyzer.md # This documentation file +``` +``` diff --git a/facebook_analyzer/__init__.py b/facebook_analyzer/__init__.py new file mode 100644 index 0000000..c4c716a --- /dev/null +++ b/facebook_analyzer/__init__.py @@ -0,0 +1,8 @@ +# This file makes the 'facebook_analyzer' directory a Python package. +# You can leave it empty or add package-level imports here if needed later. + +# For example, you might want to make functions from modules directly available: +# from .phishing_detector import analyze_message_for_phishing +# from .fake_profile_detector import analyze_profile_for_fakeness + +# For now, keeping it simple. User will import specific modules. diff --git a/facebook_analyzer/__pycache__/__init__.cpython-312.pyc b/facebook_analyzer/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..5e755b3 Binary files /dev/null and b/facebook_analyzer/__pycache__/__init__.cpython-312.pyc differ diff --git a/facebook_analyzer/__pycache__/fake_profile_detector.cpython-312.pyc b/facebook_analyzer/__pycache__/fake_profile_detector.cpython-312.pyc new file mode 100644 index 0000000..df84fe0 Binary files /dev/null and b/facebook_analyzer/__pycache__/fake_profile_detector.cpython-312.pyc differ diff --git a/facebook_analyzer/__pycache__/phishing_detector.cpython-312.pyc b/facebook_analyzer/__pycache__/phishing_detector.cpython-312.pyc new file mode 100644 index 0000000..289cd83 Binary files /dev/null and b/facebook_analyzer/__pycache__/phishing_detector.cpython-312.pyc differ diff --git a/facebook_analyzer/fake_profile_detector.py b/facebook_analyzer/fake_profile_detector.py new file mode 100644 index 0000000..38872e9 --- /dev/null +++ b/facebook_analyzer/fake_profile_detector.py @@ -0,0 +1,199 @@ +# facebook_analyzer/fake_profile_detector.py + +import webbrowser + +# Common indicators of fake profiles. +# Each indicator can have a 'weight' for a simple scoring system. +# 'prompt' is what the user will be asked. +# 'type' can be 'yes_no', 'numeric', 'text_analysis' (future), etc. +# 'details_if_yes' can provide more context or ask for more info if the user answers 'yes'. +FAKE_PROFILE_INDICATORS = [ + { + "id": "profile_picture_generic", + "prompt": "Is the profile picture generic, a stock photo, an illustration, or of a celebrity (i.e., not a clear photo of a unique, real person)?", + "type": "yes_no", + "weight_if_yes": 2, + "details_if_yes": "Generic or stolen profile pictures are common for fake accounts." + }, + { + "id": "profile_picture_reverse_search", + "prompt": "Have you tried a reverse image search (e.g., Google Images, TinEye) on the profile picture? If so, did it show the image is widely used, a stock photo, or belongs to someone else?", + "type": "yes_no", + "weight_if_yes": 3, + "details_if_yes": "Reverse image search can often quickly identify stolen or common stock photos." + }, + { + "id": "account_age_very_new", + "prompt": "Does the profile seem very new with little history (e.g., join date is recent, few old posts)? (Requires manual check on the profile)", + "type": "yes_no", + "weight_if_yes": 1, + "details_if_yes": "While not definitive, many fake accounts are newly created." + }, + { + "id": "few_posts_or_activity", + "prompt": "Does the profile have very few posts, photos, or other activity over its lifespan? (Requires manual check)", + "type": "yes_no", + "weight_if_yes": 1, + "details_if_yes": "Lack of genuine activity can be a sign." + }, + { + "id": "generic_or_copied_posts", + "prompt": "Are the posts (if any) generic, nonsensical, repetitive, or seem copied from other sources? (Requires manual check)", + "type": "yes_no", + "weight_if_yes": 2, + "details_if_yes": "Content that isn't original or personal is suspicious." + }, + { + "id": "friend_count_mismatch", + "prompt": "Does the profile have a very high number of friends but very little engagement (likes/comments) on their posts, or an unusually low number of friends for a long-standing account? (Requires manual check)", + "type": "yes_no", + "weight_if_yes": 1, + "details_if_yes": "Unusual friend counts or activity ratios can be indicators." + }, + { + "id": "poor_grammar_spelling", + "prompt": "Is the language used in the profile's 'About' section or posts consistently poor in grammar or spelling (beyond typical typos)? (Requires manual check)", + "type": "yes_no", + "weight_if_yes": 1, + "details_if_yes": "Often, hastily created fake profiles have noticeable language issues." + }, + { + "id": "about_section_sparse_or_inconsistent", + "prompt": "Is the 'About' section very sparse, missing key information (like education, work), or contains information that seems inconsistent or overly glamorous/fake? (Requires manual check)", + "type": "yes_no", + "weight_if_yes": 2, + "details_if_yes": "Incomplete or suspicious 'About' information is a red flag." + }, + { + "id": "mutual_friends_suspicious", + "prompt": "If you have mutual friends, do those mutual connections seem legitimate or are they also suspicious-looking profiles?", + "type": "yes_no", + "weight_if_yes": 1, + "details_if_yes": "Fake accounts often connect with other fake accounts." + }, + { + "id": "pressure_or_strange_requests", + "prompt": "Has this profile sent you messages that pressure you for information, money, or to click suspicious links shortly after connecting?", + "type": "yes_no", + "weight_if_yes": 3, + "details_if_yes": "This is a strong indicator of a malicious fake account." + } +] + +def guide_reverse_image_search(image_url=None): + """Opens browser tabs to guide the user through reverse image search.""" + print("\n--- Guiding Reverse Image Search ---") + print("You can use services like Google Images or TinEye to check if a profile picture is used elsewhere.") + if image_url: + print(f"If you have a direct URL for the image: {image_url}") + google_url = f"https://images.google.com/searchbyimage?image_url={image_url}" + tineye_url = f"https://tineye.com/search?url={image_url}" + print(f"Attempting to open Google Images: {google_url}") + webbrowser.open(google_url) + print(f"Attempting to open TinEye: {tineye_url}") + webbrowser.open(tineye_url) + else: + print("If you have the image saved, you can upload it to these sites:") + print("Google Images: https://images.google.com/ (click the camera icon)") + webbrowser.open("https://images.google.com/") + print("TinEye: https://tineye.com/") + webbrowser.open("https://tineye.com/") + print("Look for whether the image is a common stock photo, belongs to a different person, or appears on many unrelated profiles.") + input("Press Enter to continue after performing your search...") + + +def analyze_profile_based_on_user_input(profile_url): + """ + Guides the user through a checklist to assess if a Facebook profile is fake. + Does NOT scrape any data. Relies on user observation. + """ + print(f"\n--- Analyzing Facebook Profile (Manual Check) ---") + print(f"Please open the Facebook profile in your browser: {profile_url}") + print("You will be asked a series of questions based on your observations.") + print("This tool does NOT access Facebook directly or scrape any data.") + webbrowser.open(profile_url) # Open for user convenience + + user_responses = {} + total_score = 0 + positive_indicators = [] + + # Ask about reverse image search first + perform_ris = input("Do you want guidance to perform a reverse image search on the profile picture? (yes/no): ").strip().lower() + if perform_ris == 'yes': + img_url_known = input("Do you have a direct URL for the profile image? (yes/no): ").strip().lower() + if img_url_known == 'yes': + actual_img_url = input("Please paste the direct image URL: ").strip() + guide_reverse_image_search(actual_img_url) + else: + guide_reverse_image_search() + print("Now, let's answer the question about the reverse image search based on your findings.") + + + for indicator in FAKE_PROFILE_INDICATORS: + while True: + answer = input(f"{indicator['prompt']} (yes/no): ").strip().lower() + if answer in ['yes', 'no']: + user_responses[indicator['id']] = answer + if answer == 'yes': + total_score += indicator['weight_if_yes'] + positive_indicators.append(f"- {indicator['prompt']} ({indicator['details_if_yes']})") + break + else: + print("Invalid input. Please answer 'yes' or 'no'.") + + print("\n--- Fake Profile Analysis Results ---") + print(f"Profile URL: {profile_url}") + + if not positive_indicators: + print("Based on your answers, no common fake profile indicators were strongly identified.") + print("However, always remain cautious.") + else: + print("The following indicators suggestive of a fake profile were noted based on your input:") + for pi in positive_indicators: + print(pi) + + print(f"\nOverall 'suspicion score': {total_score}") + if total_score == 0: + print("Assessment: No strong indicators noted from your input.") + elif total_score <= 3: + print("Assessment: Low likelihood of being fake based on your input, but remain cautious.") + elif total_score <= 6: + print("Assessment: Medium likelihood. Some indicators suggest this profile could be fake. Exercise caution.") + elif total_score <= 9: + print("Assessment: High likelihood. Several indicators suggest this profile may be fake. High caution advised.") + else: + print("Assessment: Very high likelihood. Many strong indicators suggest this profile is likely fake. Avoid interaction and consider reporting.") + + print("\nDisclaimer:") + print("This analysis is based SOLELY on your manual observations and answers to the checklist.") + print("It is not a definitive judgment. False positives and negatives are possible.") + print("Always use your best judgment when interacting with profiles online.") + print("If you suspect a profile is fake and malicious, consider reporting it to Facebook through their official channels.") + + return { + "profile_url": profile_url, + "score": total_score, + "positive_indicators_details": positive_indicators, + "user_responses": user_responses + } + +if __name__ == '__main__': + print("Fake Profile Detector - Manual Checklist Tool") + print("IMPORTANT: This tool does NOT access Facebook or scrape data.") + print("It guides YOU to manually check a profile and answer questions.") + print("------------------------------------------------------------") + + # Example of how it would be called: + # First, ensure the user is aware of the process for reverse image search, as it's a common first step. + # For the test, we'll simulate this. + + test_profile_url = input("Enter a Facebook profile URL to simulate analyzing (e.g., https://www.facebook.com/some.profile): ").strip() + if not test_profile_url: + print("No URL entered, exiting.") + else: + # In a real CLI, you might ask about reverse image search separately first, or integrate it. + # For this direct test, the function itself will ask. + analysis = analyze_profile_based_on_user_input(test_profile_url) + # print("\nFull analysis object (for debugging):") + # import json + # print(json.dumps(analysis, indent=2)) diff --git a/facebook_analyzer/phishing_detector.py b/facebook_analyzer/phishing_detector.py new file mode 100644 index 0000000..f5b65ed --- /dev/null +++ b/facebook_analyzer/phishing_detector.py @@ -0,0 +1,206 @@ +import re + +# Keywords common in phishing messages +PHISHING_KEYWORDS = [ + "verify your account", "update your details", "confirm your identity", + "login required", "secure your account", "account suspended", + "unusual activity", "security alert", "important notification", + "action required", "limited time offer", "winner", "prize", + "confidential", "urgent", "immediate attention", "access restricted", + "card declined", "payment issue", "invoice", "refund" +] + +# Patterns for suspicious URLs +# Order matters: more specific/dangerous patterns should come first. +SUSPICIOUS_URL_PATTERNS = [ + # Attempts to impersonate legitimate domains by using them as subdomains of a malicious domain + # e.g., facebook.com.malicious.com, login-facebook.com-site.org + r"https?://(?:[a-z0-9\-]+\.)*(?:facebook|fb|instagram|whatsapp)\.com\.[a-z0-9\-]+\.[a-z]+", + r"https?://(?:[a-z0-9\-]+\.)*facebook-[a-z0-9\-]+\.[a-z]+", + r"https?://(?:[a-z0-9\-]+\.)*fb-[a-z0-9\-]+\.[a-z]+", + # Common URL shorteners (can be legitimate but often used in phishing) + r"https?://bit\.ly", + r"https?://goo\.gl", + r"https?://t\.co", # Twitter shortener, often abused + # IP Address URLs + r"https?://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", + # Generic keywords in domain that are often suspicious if not part of a known legit service + # e.g., "login", "secure", "account", "update" in a non-standard TLD or unfamiliar domain + r"https?://[^/]*(?:login|secure|account|update|verify|support|admin)[^/]*\.(?:biz|info|tk|ml|ga|cf|gq|xyz|club|top|loan|work|online|site)", + # Very long subdomains or many hyphens (common obfuscation) + r"https?://(?:[a-z0-9\-]+\.){4,}", # 4 or more subdomains + r"https?://[^/]*\-.*\-.*\-.*[a-z]+", # multiple hyphens in domain part +] + +LEGITIMATE_DOMAINS = [ + "facebook.com", + "www.facebook.com", + "m.facebook.com", + "fb.com", # Official Facebook shortener + "www.fb.com", + "instagram.com", + "www.instagram.com", + "whatsapp.com", + "www.whatsapp.com", + "google.com", # For test cases + "www.google.com", + "amazon.com", # For test cases + "www.amazon.com" +] + +def extract_urls(text): + """Extracts URLs from a given text.""" + url_pattern = r'https?://[^\s<>"]+|www\.[^\s<>"]+' + return re.findall(url_pattern, text) + +def get_domain_from_url(url): + """Extracts the domain (e.g., 'example.com') from a URL.""" + if "://" in url: + domain = url.split("://")[1].split("/")[0].split("?")[0] + else: # Handles www.example.com cases without http(s) + domain = url.split("/")[0].split("?")[0] + return domain.lower() + +def is_url_suspicious(url): + """ + Checks if a URL is suspicious. + Returns a tuple: (bool_is_suspicious, reason_string) + """ + normalized_url_for_pattern_matching = url.lower() + domain = get_domain_from_url(url) + + # 1. Check against explicit legitimate domains + # This is a strong signal that it *might* be okay, but phishing can still occur on legit sites (e.g., compromised page). + # However, for this tool, if the *domain itself* is legit, we'll primarily rely on other indicators for now. + if domain in LEGITIMATE_DOMAINS: + # We could add checks here for suspicious paths on legitimate domains, + # but that's more complex. For now, if the core domain is legit, + # we won't flag it based on domain alone. + # Let's still check if it matches any *very specific* impersonation patterns + # that might accidentally include a legit domain name within them. + for pattern in [ + r"https?://(?:[a-z0-9\-]+\.)*(?:facebook|fb|instagram|whatsapp)\.com\.[a-z0-9\-]+\.[a-z]+", #e.g. facebook.com.hacker.com + r"https?://(?:[a-z0-9\-]+\.)*facebook-[a-z0-9\-]+\.[a-z]+" #e.g. my-facebook-login.hacker.com + ]: + if re.search(pattern, normalized_url_for_pattern_matching, re.IGNORECASE): + # Check if the *actual domain* is the legit one, not just contained. + # e.g. "facebook.com.hacker.com" contains "facebook.com" but domain is "hacker.com" + if not domain.endswith("facebook.com"): # Simplified check for this example + return True, f"URL impersonates a legitimate domain: {pattern}" + return False, "URL domain is on the legitimate list." + + # 2. Check against known suspicious patterns (these should be more specific) + for pattern in SUSPICIOUS_URL_PATTERNS: + if re.search(pattern, normalized_url_for_pattern_matching, re.IGNORECASE): + return True, f"URL matches suspicious pattern: {pattern}" + + # 3. Heuristic: Check if a known legitimate domain name is *part* of the domain, + # but the domain itself is NOT on the legitimate list. + # E.g., "facebook-login.some-other-site.com" + for legit_substring in ["facebook", "fb", "instagram", "whatsapp"]: + if legit_substring in domain: + # We already checked if `domain` is in `LEGITIMATE_DOMAINS`. + # So if we're here, it means `legit_substring` is in `domain`, but `domain` itself is not legit. + return True, f"URL contains name of a legitimate service ('{legit_substring}') but is not an official domain." + + return False, "URL does not match common suspicious patterns and is not on the explicit legitimate list." + + +def analyze_message_for_phishing(message_text): + """ + Analyzes a message for phishing indicators. + Returns a dictionary with findings. + """ + findings = { + "score": 0, # Overall phishing likelihood score (higher is more suspicious) + "keywords_found": [], + "suspicious_urls_found": [], + "urls_extracted": [], + "summary": "" + } + + # 1. Analyze text for keywords + message_lower = message_text.lower() + for keyword in PHISHING_KEYWORDS: + if keyword in message_lower: + findings["keywords_found"].append(keyword) + findings["score"] += 1 + + # 2. Extract and analyze URLs + urls = extract_urls(message_text) + findings["urls_extracted"] = urls + for url in urls: + is_susp, reason = is_url_suspicious(url) + if is_susp: + findings["suspicious_urls_found"].append({"url": url, "reason": reason}) + findings["score"] += 2 # Higher weight for suspicious URLs + + # 3. Generate summary + if not findings["keywords_found"] and not findings["suspicious_urls_found"]: + findings["summary"] = "No immediate phishing indicators found. However, always exercise caution with links and requests for information." + else: + summary_parts = [] + if findings["keywords_found"]: + summary_parts.append(f"Found {len(findings['keywords_found'])} suspicious keyword(s): {', '.join(findings['keywords_found'])}.") + if findings["suspicious_urls_found"]: + summary_parts.append(f"Found {len(findings['suspicious_urls_found'])} suspicious URL(s).") + for sus_url in findings["suspicious_urls_found"]: + summary_parts.append(f" - {sus_url['url']} (Reason: {sus_url['reason']})") + + findings["summary"] = " ".join(summary_parts) + if findings["score"] > 0: + findings["summary"] += f" Overall phishing score: {findings['score']} (higher is more suspicious)." + + + return findings + +if __name__ == '__main__': + # Example Usage + original_test_messages = [ + ("URGENT: Your Facebook account has unusual activity. Please verify your account now by clicking http://facebook.security-update.com/login to avoid suspension.", "Original 1"), + ("Hey, check out this cool site: www.google.com", "Original 2"), + ("Your package is waiting for delivery. Update your shipping details here: http://bit.ly/fakepackage", "Original 3"), + ("Hi, this is your bank. We need you to confirm your identity due to a login required. Please visit https://mybank.secure-access-point.net/confirm", "Original 4"), + ("A login to your account from a new device was detected. If this wasn't you, please secure your account at http://123.45.67.89/facebook_login", "Original 5"), + ("Click here to claim your prize! http://winner.com/prize-claim-form-xyz", "Original 6"), + ("Official communication from Facebook: Please review our new terms at https://facebook.com/terms. This is important for your account security.", "Original 7") + ] + + additional_test_messages = [ + ("Security Alert! Update your info at http://facebook.com.hacker.com and also check this http://bit.ly/anotherlink", "Additional 1: Multiple suspicious URLs"), + ("URGENT: verify your account at https://facebook.com/security/alerts - this is a real link, but also check http://mysecurity-fb-check.com", "Additional 2: Mix of legit FB URL and suspicious one with keywords"), + ("Hello there, how are you doing today?", "Additional 3: No keywords, no URLs"), + ("Important security update from Facebook. Please login at https://www.facebook.com to review your settings. Your account safety is our priority.", "Additional 4: Keywords but legit URL"), + ("Check this out: http://bit.ly/legitGoogleDoc - this could be a legit shortened link (hard to tell without unshortening)", "Additional 5: URL shortener, potentially legit content") + ] + + all_test_messages = original_test_messages + additional_test_messages + + for i, (msg, label) in enumerate(all_test_messages): + print(f"--- Analyzing Message ({label}) ---") + print(f"Message: {msg}") + analysis_result = analyze_message_for_phishing(msg) + print(f"Score: {analysis_result['score']}") + print(f"Keywords: {analysis_result['keywords_found']}") + print(f"Suspicious URLs: {analysis_result['suspicious_urls_found']}") + print(f"All URLs: {analysis_result['urls_extracted']}") + print(f"Summary: {analysis_result['summary']}") + print("-" * 30 + "\n") + + # Test URL suspicion logic directly + print("\n--- Testing URL Suspicion Logic ---") + test_urls = [ + "http://facebook.com.malicious.com/login.html", + "https://www.facebook.com/officialpage", + "http://fb.com-security-alert.com", + "https://legit-service.com/facebook_integration", # Might be ok + "http://192.168.1.10/phish", + "https.google.com", + "www.amazon.com/deals", + "http://bit.ly/randomstuff", + "https://totally-not-facebook.com", + "http://facebook.com" # Should not be suspicious by default + ] + for url in test_urls: + is_susp, reason = is_url_suspicious(url) + print(f"URL: {url} -> Suspicious: {is_susp}, Reason: {reason}") diff --git a/iot_sim_main.py b/iot_sim_main.py new file mode 100644 index 0000000..fbf0efd --- /dev/null +++ b/iot_sim_main.py @@ -0,0 +1,197 @@ +import argparse +import time +import json # For parsing device_profiles if given as JSON string +import uuid +from iot_simulator.generators import generate_sensor_data, generate_timestamp, DEVICE_STATES +from iot_simulator.publishers import format_payload, print_to_console, send_http_post + +def parse_device_profiles(profiles_str_list): + """ + Parses device profile strings. + Each string can be "device_id:sensor1,sensor2" or just "sensor1,sensor2" (auto-gen ID). + Or, it can be a JSON string representing a list of more complex profiles. + """ + profiles = [] + if not profiles_str_list: + return profiles + + # Check if the input is a single string that might be JSON + if len(profiles_str_list) == 1 and (profiles_str_list[0].startswith('[') or profiles_str_list[0].startswith('{')): + try: + parsed_json = json.loads(profiles_str_list[0]) + if isinstance(parsed_json, list): + # Expecting list of {"id": "dev1", "sensors": ["temp", "hum"]} + for p_item in parsed_json: + if isinstance(p_item, dict) and "id" in p_item and "sensors" in p_item: + profiles.append({"id": p_item["id"], "sensors": p_item["sensors"]}) + else: + print(f"Warning: Invalid JSON profile item format: {p_item}. Skipping.") + return profiles + elif isinstance(parsed_json, dict): # Single profile as JSON object + if "id" in parsed_json and "sensors" in parsed_json: + profiles.append({"id": parsed_json["id"], "sensors": parsed_json["sensors"]}) + return profiles + else: + print(f"Warning: Invalid JSON profile object format: {parsed_json}. Skipping.") + return [] # Or handle as error + except json.JSONDecodeError as e: + print(f"Warning: Could not parse profile string as JSON '{profiles_str_list[0]}': {e}. Proceeding with string parsing.") + # Fall through to string parsing if JSON attempt fails for a single ambiguous string + + # String parsing for "device_id:sensor1,sensor2" or "sensor1,sensor2" + for profile_str in profiles_str_list: + parts = profile_str.split(':', 1) + device_id = "" + sensors_str = "" + + if len(parts) == 2: + device_id = parts[0].strip() + sensors_str = parts[1].strip() + elif len(parts) == 1: + sensors_str = parts[0].strip() + # No device_id provided, will auto-generate + else: + print(f"Warning: Invalid profile string format '{profile_str}'. Skipping.") + continue + + if not device_id: + device_id = f"sim-{uuid.uuid4().hex[:8]}" + print(f"Auto-generated device ID: {device_id} for sensors: {sensors_str}") + + sensor_types = [s.strip() for s in sensors_str.split(',') if s.strip()] + if not sensor_types: + print(f"Warning: No sensor types specified for device ID '{device_id}' in profile '{profile_str}'. Skipping.") + continue + + profiles.append({"id": device_id, "sensors": sensor_types}) + + return profiles + + +def main_loop(profiles, interval_seconds, num_messages, output_target, http_url=None): + """ + Main simulation loop. + """ + if not profiles: + print("No valid device profiles configured. Exiting.") + return + + print(f"\nStarting IoT simulation...") + print(f"Device Profiles: {profiles}") + print(f"Interval: {interval_seconds}s") + print(f"Messages per run (per profile): {num_messages if num_messages > 0 else 'Infinite'}") + print(f"Output Target: {output_target}") + if output_target == 'http' and http_url: + print(f"HTTP Target URL: {http_url}") + + # Reset global device states for a fresh run if needed, or manage them per profile run. + # For simplicity, DEVICE_STATES in generators.py is global. + # If we want truly independent runs for counters/GPS per main_loop call, clear it here. + # DEVICE_STATES.clear() # Uncomment if each run of main_loop should reset all device states + + try: + msg_count = 0 + while True: + if num_messages > 0 and msg_count >= num_messages: + print(f"\nReached target message count ({num_messages}). Simulation finished for this run.") + break + + current_ts = generate_timestamp() + + for profile in profiles: + device_id = profile["id"] + sensor_types = profile["sensors"] + + # This list will hold data from multiple sensors for this device for this timestamp + all_sensor_readings_for_device = [] + + for sensor_type in sensor_types: + # generate_sensor_data from generators.py manages state per device_id + reading = generate_sensor_data(device_id, sensor_type) + if reading: + all_sensor_readings_for_device.append(reading) + + if not all_sensor_readings_for_device: + print(f"Warning: No sensor data generated for device {device_id} at {current_ts}. Skipping.") + continue + + # Format the payload with all readings for this device + payload = format_payload(device_id, current_ts, all_sensor_readings_for_device) + + if output_target == 'console': + print_to_console(payload) + elif output_target == 'http': + if http_url: + send_http_post(http_url, payload) + else: + print("Error: HTTP output specified but no URL provided. Skipping send.") + + msg_count += 1 + if num_messages == 0 or msg_count < num_messages : # Only sleep if not the last message of a finite run + print(f"--- Sent message batch #{msg_count}. Waiting {interval_seconds}s... ---") + time.sleep(interval_seconds) + + except KeyboardInterrupt: + print("\nSimulation stopped by user (Ctrl+C).") + except Exception as e: + print(f"\nAn unexpected error occurred during simulation: {e}") + finally: + print("IoT Simulation ended.") + + +def main(): + parser = argparse.ArgumentParser(description="Generic IoT Data Simulator.") + + parser.add_argument( + "-p", "--profiles", + nargs='+', + required=True, + help='Device profiles. Each profile as "device_id:sensor1,sensor2,..." or "sensor1,sensor2" (ID auto-generated). ' + 'Alternatively, a single argument which is a JSON string: \'[{"id":"dev1","sensors":["temp","hum"]}]\' ' + 'Supported sensors: temperature, humidity, gps, status, counter.' + ) + parser.add_argument( + "-i", "--interval", + type=float, + default=5.0, + help="Interval in seconds between sending messages (default: 5.0s)." + ) + parser.add_argument( + "-n", "--num_messages", + type=int, + default=10, + help="Number of messages to send per simulation run (0 for infinite) (default: 10)." + ) + parser.add_argument( + "-o", "--output", + choices=['console', 'http'], + default='console', + help="Output target: 'console' or 'http' (default: console)." + ) + parser.add_argument( + "--http_url", + help="Target URL for HTTP POST output (required if --output=http)." + ) + + args = parser.parse_args() + + if args.output == 'http' and not args.http_url: + parser.error("--http_url is required when --output is 'http'.") + + parsed_profiles = parse_device_profiles(args.profiles) + if not parsed_profiles: + print("Error: No valid device profiles could be parsed. Please check your --profiles argument.") + print("Examples: --profiles \"myDevice:temperature,humidity\" \"another:gps,counter\"") + print(" or: --profiles '[{\"id\":\"dev1\",\"sensors\":[\"temperature\",\"status\"]}, {\"id\":\"dev2\",\"sensors\":[\"gps\"]}]'") + return + + main_loop( + profiles=parsed_profiles, + interval_seconds=args.interval, + num_messages=args.num_messages, + output_target=args.output, + http_url=args.http_url + ) + +if __name__ == "__main__": + main() diff --git a/iot_simulator/__init__.py b/iot_simulator/__init__.py new file mode 100644 index 0000000..86e95d8 --- /dev/null +++ b/iot_simulator/__init__.py @@ -0,0 +1,15 @@ +# This file makes 'iot_simulator' a Python package. + +from .generators import ( + generate_temperature, + generate_humidity, + generate_gps_coordinates, + generate_boolean_status, + generate_counter_value, + generate_timestamp, + generate_device_id, + generate_sensor_data, + DEVICE_STATES # Exposing for potential external state management if ever needed, or reset +) + +from .publishers import format_payload, print_to_console, send_http_post diff --git a/iot_simulator/__pycache__/__init__.cpython-312.pyc b/iot_simulator/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..d622ffc Binary files /dev/null and b/iot_simulator/__pycache__/__init__.cpython-312.pyc differ diff --git a/iot_simulator/__pycache__/generators.cpython-312.pyc b/iot_simulator/__pycache__/generators.cpython-312.pyc new file mode 100644 index 0000000..520f69a Binary files /dev/null and b/iot_simulator/__pycache__/generators.cpython-312.pyc differ diff --git a/iot_simulator/__pycache__/publishers.cpython-312.pyc b/iot_simulator/__pycache__/publishers.cpython-312.pyc new file mode 100644 index 0000000..aee507b Binary files /dev/null and b/iot_simulator/__pycache__/publishers.cpython-312.pyc differ diff --git a/iot_simulator/generators.py b/iot_simulator/generators.py new file mode 100644 index 0000000..d12a32a --- /dev/null +++ b/iot_simulator/generators.py @@ -0,0 +1,174 @@ +import random +import datetime +import uuid + +# --- Configuration for sensor data ranges/values --- +DEFAULT_TEMP_RANGE_CELSIUS = (-10.0, 40.0) +DEFAULT_HUMIDITY_RANGE_PERCENT = (20.0, 80.0) + +# Base GPS coordinates (e.g., a central point for random walks) +# For more realism, this could be configurable per device. +BASE_LATITUDE = 34.0522 # Los Angeles +BASE_LONGITUDE = -118.2437 +# Max random walk step for GPS per update +GPS_MAX_STEP = 0.0005 # Approx 50 meters + +# --- Generator Functions --- + +def generate_temperature(min_temp=DEFAULT_TEMP_RANGE_CELSIUS[0], max_temp=DEFAULT_TEMP_RANGE_CELSIUS[1]): + """Generates a random temperature reading.""" + return round(random.uniform(min_temp, max_temp), 2) + +def generate_humidity(min_humidity=DEFAULT_HUMIDITY_RANGE_PERCENT[0], max_humidity=DEFAULT_HUMIDITY_RANGE_PERCENT[1]): + """Generates a random humidity reading.""" + return round(random.uniform(min_humidity, max_humidity), 2) + +def generate_gps_coordinates(current_lat=None, current_lon=None): + """ + Generates GPS coordinates, optionally performing a random walk from previous coordinates. + If no current coordinates are provided, starts from BASE_LATITUDE, BASE_LONGITUDE. + """ + if current_lat is None or current_lon is None: + lat = BASE_LATITUDE + lon = BASE_LONGITUDE + else: + lat = current_lat + random.uniform(-GPS_MAX_STEP, GPS_MAX_STEP) + lon = current_lon + random.uniform(-GPS_MAX_STEP, GPS_MAX_STEP) + + # Clamp to valid GPS ranges + lat = max(min(lat, 90.0), -90.0) + lon = max(min(lon, 180.0), -180.0) + + return {"latitude": round(lat, 6), "longitude": round(lon, 6)} + +def generate_boolean_status(): + """Generates a random boolean status (True/False).""" + return random.choice([True, False]) + +def generate_counter_value(current_value=0): + """Increments a counter value.""" + return current_value + 1 + +def generate_timestamp(): + """Generates an ISO 8601 formatted timestamp in UTC.""" + return datetime.datetime.now(datetime.timezone.utc).isoformat() + +def generate_device_id(prefix="sim"): + """Generates a unique device ID.""" + return f"{prefix}-{uuid.uuid4().hex[:8]}" + + +# --- Main Data Generation Orchestrator (per device state) --- + +# Store last known values for sensors that evolve (like GPS, counter) +# This would typically be managed per device instance in the main simulator loop. +# For testing here, we can use a global-like dictionary. +DEVICE_STATES = {} # Key: device_id, Value: dict of sensor states (e.g., last_lat, last_lon, last_count) + +def generate_sensor_data(device_id, sensor_type_config): + """ + Generates data for a specific sensor type, maintaining state if necessary. + + Args: + device_id (str): The ID of the device for which to generate data. + sensor_type_config (str or dict): + If str: 'temperature', 'humidity', 'gps', 'status', 'counter'. + If dict: Can provide specific params, e.g., {'type': 'temperature', 'range': [0, 30]} + (Not fully implemented for MVP, uses defaults for now). + + Returns: + dict: A dictionary containing the sensor type as key and generated value. + e.g., {'temperature_celsius': 25.5} or {'gps': {'latitude': ..., 'longitude': ...}} + Returns None if sensor type is unknown. + """ + if device_id not in DEVICE_STATES: + DEVICE_STATES[device_id] = { + "counter": 0, + "latitude": None, # Start GPS from base if no prior state + "longitude": None + } + + sensor_type = sensor_type_config + # TODO: Add parsing for sensor_type_config if it's a dict for custom ranges in future. + + if sensor_type == 'temperature': + return {'temperature_celsius': generate_temperature()} + elif sensor_type == 'humidity': + return {'humidity_percent': generate_humidity()} + elif sensor_type == 'gps': + coords = generate_gps_coordinates( + DEVICE_STATES[device_id].get('latitude'), + DEVICE_STATES[device_id].get('longitude') + ) + DEVICE_STATES[device_id]['latitude'] = coords['latitude'] + DEVICE_STATES[device_id]['longitude'] = coords['longitude'] + return {'location': coords} + elif sensor_type == 'status': + # Example: could be named based on config, e.g., "door_status" + return {'active_status': generate_boolean_status()} + elif sensor_type == 'counter': + DEVICE_STATES[device_id]['counter'] = generate_counter_value(DEVICE_STATES[device_id]['counter']) + return {'event_count': DEVICE_STATES[device_id]['counter']} + else: + print(f"Warning: Unknown sensor type '{sensor_type}' requested for device '{device_id}'.") + return None + +if __name__ == '__main__': + print("--- Testing Individual Generators ---") + print(f"Temperature: {generate_temperature()} °C") + print(f"Humidity: {generate_humidity()} %") + + gps_coords1 = generate_gps_coordinates() + print(f"Initial GPS: {gps_coords1}") + gps_coords2 = generate_gps_coordinates(gps_coords1['latitude'], gps_coords1['longitude']) + print(f"Next GPS (walk): {gps_coords2}") + + print(f"Boolean Status: {generate_boolean_status()}") + print(f"Counter (start 0): {generate_counter_value(0)}") + print(f"Counter (start 10): {generate_counter_value(10)}") + print(f"Timestamp: {generate_timestamp()}") + print(f"Device ID: {generate_device_id()}") + print(f"Device ID (prefix 'dev'): {generate_device_id(prefix='health_sensor')}") + + print("\n--- Testing generate_sensor_data (stateful) ---") + dev1 = "simDeviceTest001" + print(f"\nDevice: {dev1}") + + sensor_reading = generate_sensor_data(dev1, 'temperature') + print(f"Sensor data: {sensor_reading}") + assert 'temperature_celsius' in sensor_reading + + sensor_reading = generate_sensor_data(dev1, 'gps') + print(f"Sensor data: {sensor_reading}") + assert 'location' in sensor_reading + + sensor_reading = generate_sensor_data(dev1, 'gps') # Second GPS reading for same device + print(f"Sensor data (GPS again): {sensor_reading}") + assert sensor_reading['location']['latitude'] != BASE_LATITUDE or sensor_reading['location']['longitude'] != BASE_LONGITUDE + + sensor_reading = generate_sensor_data(dev1, 'counter') + print(f"Sensor data: {sensor_reading}") + assert sensor_reading['event_count'] == 1 + + sensor_reading = generate_sensor_data(dev1, 'counter') + print(f"Sensor data (Counter again): {sensor_reading}") + assert sensor_reading['event_count'] == 2 + + sensor_reading = generate_sensor_data(dev1, 'status') + print(f"Sensor data: {sensor_reading}") + assert 'active_status' in sensor_reading + + sensor_reading = generate_sensor_data(dev1, 'unknown_sensor') + assert sensor_reading is None + + # Test state for a different device + dev2 = "simDeviceTest002" + print(f"\nDevice: {dev2}") + sensor_reading = generate_sensor_data(dev2, 'counter') + print(f"Sensor data: {sensor_reading}") + assert sensor_reading['event_count'] == 1 + sensor_reading = generate_sensor_data(dev1, 'counter') # dev1 counter should be unaffected + print(f"Sensor data (dev1 Counter check): {sensor_reading}") + assert sensor_reading['event_count'] == 3 + + print("\nAll basic generator tests seem to pass.") diff --git a/iot_simulator/publishers.py b/iot_simulator/publishers.py new file mode 100644 index 0000000..72c3ac9 --- /dev/null +++ b/iot_simulator/publishers.py @@ -0,0 +1,135 @@ +import json +import requests +import time +import random # <--- Import random + +def format_payload(device_id, timestamp, sensor_data_list): + """ + Formats the final JSON payload. + Sensor_data_list is a list of dicts, e.g., [{'temperature_celsius': 22.5}, {'humidity_percent': 45}] + These will be merged into a single data object. + """ + payload = { + "deviceId": device_id, + "timestamp": timestamp + } + # Merge all sensor data dictionaries into the main payload + for sensor_dict in sensor_data_list: + if sensor_dict: # Ensure it's not None (e.g., from an unknown sensor type) + payload.update(sensor_dict) + return payload + +def print_to_console(payload): + """Prints the JSON payload to the console.""" + try: + print(json.dumps(payload, indent=2)) + return True + except Exception as e: + print(f"Error printing to console: {e}") + return False + +def send_http_post(url, payload, timeout=10): + """ + Sends the JSON payload via HTTP POST to the specified URL. + + Args: + url (str): The target URL. + payload (dict): The data payload to send. + timeout (int): Request timeout in seconds. + + Returns: + bool: True if the request was successful (e.g., 2xx status code), False otherwise. + """ + if not url: + print("Error: HTTP target URL not specified.") + return False + + headers = {'Content-Type': 'application/json'} + try: + response = requests.post(url, data=json.dumps(payload), headers=headers, timeout=timeout) + response.raise_for_status() # Raises an HTTPError for bad responses (4XX or 5XX) + print(f"Data successfully sent to {url}. Status: {response.status_code}") + return True + except requests.exceptions.HTTPError as e: + print(f"HTTP error sending data to {url}: {e.response.status_code} {e.response.reason}") + print(f"Response body: {e.response.text}") + except requests.exceptions.ConnectionError as e: + print(f"Connection error sending data to {url}: {e}") + except requests.exceptions.Timeout as e: + print(f"Timeout sending data to {url}: {e}") + except requests.exceptions.RequestException as e: + print(f"An unexpected error occurred sending data to {url}: {e}") + return False + +if __name__ == '__main__': + # Test data (mimicking what the main simulator loop would provide) + test_device_id = "simTestDevice001" + + # Simulate what generate_timestamp() would do + from datetime import datetime, timezone + test_timestamp = datetime.now(timezone.utc).isoformat() + + # Simulate what generate_sensor_data() would produce for a list of sensors + test_sensor_data_temp = {'temperature_celsius': 25.5} + test_sensor_data_humidity = {'humidity_percent': 55.2} + test_sensor_data_gps = {'location': {'latitude': 34.05, 'longitude': -118.24}} + test_sensor_data_list = [test_sensor_data_temp, test_sensor_data_humidity, test_sensor_data_gps] + + # --- Test format_payload --- + print("--- Testing Payload Formatting ---") + formatted_payload = format_payload(test_device_id, test_timestamp, test_sensor_data_list) + print("Formatted Payload:") + print_to_console(formatted_payload) # Also tests print_to_console + assert formatted_payload["deviceId"] == test_device_id + assert "temperature_celsius" in formatted_payload + assert "location" in formatted_payload + assert isinstance(formatted_payload["location"], dict) + print("Payload formatting test passed.") + + # --- Test print_to_console (implicitly tested above) --- + print("\n--- Testing Console Output (already seen above) ---") + success_print = print_to_console({"message": "Test console print"}) + assert success_print + print("Console output test passed (check output above).") + + + # --- Test send_http_post --- + # For this test to actually send, you need a local server listening + # or use a service like https://beeceptor.com or https://requestbin.com + # Example: python -m http.server 8080 (will show POST requests in its log) + + print("\n--- Testing HTTP POST ---") + # test_http_url = "http://localhost:8080" # For local testing with `python -m http.server 8080` + # test_http_url = "https://httpbin.org/post" # A public endpoint that echoes requests + test_http_url_beeceptor = "https://jules-iot-test.free.beeceptor.com/mydata" # My temporary Beeceptor endpoint + + print(f"Attempting to send data to: {test_http_url_beeceptor}") + print("If this URL is active, you should see the request there.") + print("This test will likely print success if the endpoint is reachable and returns 2xx.") + + http_payload = { + "deviceId": "httpTestDevice", + "timestamp": datetime.now(timezone.utc).isoformat(), + "test_value": random.randint(1, 100), + "message": "Hello from IoT Simulator Test" + } + + # Note: In a sandbox without internet, this will likely fail with ConnectionError. + # The goal here is to ensure the function structure is correct. + success_http = send_http_post(test_http_url_beeceptor, http_payload) + if success_http: + print("HTTP POST test reported success (check Beeceptor or your test endpoint).") + else: + print("HTTP POST test reported failure (this is expected if no internet/server).") + + print("\n--- Testing HTTP POST with invalid URL ---") + success_http_invalid = send_http_post("http://nonexistentfakedomain123abc.com/api", http_payload) + assert not success_http_invalid # Should fail + print("HTTP POST to invalid URL test passed (expected failure).") + + print("\n--- Testing HTTP POST with no URL ---") + success_http_no_url = send_http_post("", http_payload) + assert not success_http_no_url + print("HTTP POST with no URL test passed (expected failure).") + + print("\nPublisher tests complete.") diff --git a/iot_simulator_requirements.txt b/iot_simulator_requirements.txt new file mode 100644 index 0000000..4a5625c --- /dev/null +++ b/iot_simulator_requirements.txt @@ -0,0 +1 @@ +requests>=2.25.0 diff --git a/main.py b/main.py new file mode 100644 index 0000000..5612951 --- /dev/null +++ b/main.py @@ -0,0 +1,68 @@ +import sys +from facebook_analyzer import phishing_detector +from facebook_analyzer import fake_profile_detector + +def display_menu(): + """Displays the main menu to the user.""" + print("\n--- Facebook Security Analyzer ---") + print("Choose an option:") + print("1. Analyze a message for phishing") + print("2. Analyze a Facebook profile for fakeness (manual check)") + print("3. Exit") + print("------------------------------------") + +def main(): + """Main function to run the CLI application.""" + while True: + display_menu() + choice = input("Enter your choice (1-3): ").strip() + + if choice == '1': + print("\n--- Phishing Message Analyzer ---") + message_text = input("Paste the full message text you want to analyze:\n") + if not message_text: + print("No message text provided. Returning to menu.") + continue + + print("\nAnalyzing message...") + analysis_result = phishing_detector.analyze_message_for_phishing(message_text) + + print("\n--- Phishing Analysis Results ---") + print(f"Score: {analysis_result['score']} (Higher is more suspicious)") + if analysis_result['keywords_found']: + print(f"Suspicious Keywords Found: {', '.join(analysis_result['keywords_found'])}") + if analysis_result['suspicious_urls_found']: + print("Suspicious URLs Found:") + for sus_url in analysis_result['suspicious_urls_found']: + print(f" - URL: {sus_url['url']}") + print(f" Reason: {sus_url['reason']}") + if not analysis_result['keywords_found'] and not analysis_result['suspicious_urls_found']: + print("No specific phishing keywords or suspicious URLs detected by basic checks.") + + print(f"\nOverall Summary: {analysis_result['summary']}") + print("---------------------------------") + + elif choice == '2': + print("\n--- Fake Profile Analyzer (Manual Check) ---") + profile_url = input("Enter the full Facebook profile URL you want to analyze (e.g., https://www.facebook.com/username):\n").strip() + if not profile_url.startswith("http://") and not profile_url.startswith("https://"): + print("Invalid URL format. Please include http:// or https://. Returning to menu.") + continue + if not profile_url: + print("No profile URL provided. Returning to menu.") + continue + + fake_profile_detector.analyze_profile_based_on_user_input(profile_url) + print("------------------------------------------") + + elif choice == '3': + print("Exiting Facebook Security Analyzer. Stay safe!") + sys.exit() + + else: + print("Invalid choice. Please enter a number between 1 and 3.") + + input("\nPress Enter to return to the main menu...") + +if __name__ == '__main__': + main() diff --git a/map.gexf b/map.gexf new file mode 100644 index 0000000..bbd2a15 --- /dev/null +++ b/map.gexf @@ -0,0 +1,22 @@ + + + + NetworkX 3.5 + + + + + + + + + + + + + + + + + + diff --git a/map.graphml b/map.graphml new file mode 100644 index 0000000..85857ad --- /dev/null +++ b/map.graphml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/map_all.gexf b/map_all.gexf new file mode 100644 index 0000000..bbd2a15 --- /dev/null +++ b/map_all.gexf @@ -0,0 +1,22 @@ + + + + NetworkX 3.5 + + + + + + + + + + + + + + + + + + diff --git a/map_all.graphml b/map_all.graphml new file mode 100644 index 0000000..85857ad --- /dev/null +++ b/map_all.graphml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/map_all.png b/map_all.png new file mode 100644 index 0000000..f8f48cb Binary files /dev/null and b/map_all.png differ diff --git a/map_default.png b/map_default.png new file mode 100644 index 0000000..b70b233 Binary files /dev/null and b/map_default.png differ diff --git a/map_kk.png b/map_kk.png new file mode 100644 index 0000000..8f0a10b Binary files /dev/null and b/map_kk.png differ diff --git a/netmap_tool/__init__.py b/netmap_tool/__init__.py new file mode 100644 index 0000000..6ce3ab4 --- /dev/null +++ b/netmap_tool/__init__.py @@ -0,0 +1,6 @@ +# This file makes 'netmap_tool' a Python package. +# It can be left empty or used to expose functions from modules. +from .mapper import build_graph_from_csv +# Placeholder for other functions if we add them later +# from .mapper import draw_network_graph, export_graph_gexf +# These will be uncommented when the functions are defined. diff --git a/netmap_tool/__pycache__/__init__.cpython-312.pyc b/netmap_tool/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..acdb730 Binary files /dev/null and b/netmap_tool/__pycache__/__init__.cpython-312.pyc differ diff --git a/netmap_tool/__pycache__/main.cpython-312.pyc b/netmap_tool/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..9134347 Binary files /dev/null and b/netmap_tool/__pycache__/main.cpython-312.pyc differ diff --git a/netmap_tool/__pycache__/mapper.cpython-312.pyc b/netmap_tool/__pycache__/mapper.cpython-312.pyc new file mode 100644 index 0000000..444b624 Binary files /dev/null and b/netmap_tool/__pycache__/mapper.cpython-312.pyc differ diff --git a/netmap_tool/main.py b/netmap_tool/main.py new file mode 100644 index 0000000..ff086d3 --- /dev/null +++ b/netmap_tool/main.py @@ -0,0 +1,61 @@ +import argparse +import os +from .mapper import build_graph_from_csv, draw_network_graph, export_graph_gexf, export_graph_graphml + +def main(): + parser = argparse.ArgumentParser(description="Network Infrastructure Mapping Tool from CSV data.") + parser.add_argument("--input_csv", required=True, help="Path to the input CSV file (e.g., links.csv). Must contain 'SourceDevice' and 'TargetDevice' columns.") + parser.add_argument("--output_png", help="Path to save the output PNG image (e.g., network_map.png).") + parser.add_argument("--layout", default="spring", choices=['spring', 'kamada_kawai', 'circular', 'random', 'shell', 'spectral'], help="Layout algorithm for the PNG map (default: spring).") + parser.add_argument("--output_gexf", help="Path to save the graph in GEXF format (e.g., network_map.gexf).") + parser.add_argument("--output_graphml", help="Path to save the graph in GraphML format (e.g., network_map.graphml).") + + args = parser.parse_args() + + # --- 1. Build graph from CSV --- + print(f"Attempting to build graph from: {args.input_csv}") + network_graph = build_graph_from_csv(args.input_csv) + + if network_graph is None: + print("Failed to build graph. Exiting.") + return + + if not network_graph.nodes(): + print("Graph built successfully, but it contains no nodes (CSV might be empty or all rows invalid). No output will be generated.") + return + + print(f"Graph built successfully: {network_graph.number_of_nodes()} nodes, {network_graph.number_of_edges()} edges.") + + # --- 2. Generate PNG output if path specified --- + if args.output_png: + print(f"Attempting to generate PNG map: {args.output_png} using {args.layout} layout...") + success_png = draw_network_graph(network_graph, args.output_png, layout_type=args.layout) + if success_png: + print(f"PNG map successfully generated: {args.output_png}") + else: + print(f"Failed to generate PNG map.") + + # --- 3. Export GEXF if path specified --- + if args.output_gexf: + print(f"Attempting to export graph to GEXF: {args.output_gexf}...") + success_gexf = export_graph_gexf(network_graph, args.output_gexf) + if success_gexf: + print(f"Graph successfully exported to GEXF: {args.output_gexf}") + else: + print(f"Failed to export graph to GEXF.") + + # --- 4. Export GraphML if path specified --- + if args.output_graphml: + print(f"Attempting to export graph to GraphML: {args.output_graphml}...") + success_graphml = export_graph_graphml(network_graph, args.output_graphml) + if success_graphml: + print(f"Graph successfully exported to GraphML: {args.output_graphml}") + else: + print(f"Failed to export graph to GraphML.") + + if not args.output_png and not args.output_gexf and not args.output_graphml: + print("No output options specified. Graph was built but no files were generated.") + print("Use --output_png, --output_gexf, or --output_graphml to save the map.") + +if __name__ == '__main__': + main() diff --git a/netmap_tool/mapper.py b/netmap_tool/mapper.py new file mode 100644 index 0000000..d674a76 --- /dev/null +++ b/netmap_tool/mapper.py @@ -0,0 +1,252 @@ +import csv +import networkx as nx +import matplotlib.pyplot as plt +import os # <-- Import the os module + +def build_graph_from_csv(csv_filepath): + """ + Reads a CSV file defining network links and builds a networkx graph. + + The CSV file should have two columns: 'SourceDevice' and 'TargetDevice'. + Each row represents a direct link between two devices. + + Args: + csv_filepath (str): The path to the input CSV file. + + Returns: + nx.Graph: A networkx graph object representing the network, or None if an error occurs. + """ + graph = nx.Graph() + nodes = set() # Keep track of all unique nodes explicitly mentioned + + try: + with open(csv_filepath, mode='r', encoding='utf-8') as csvfile: + reader = csv.DictReader(csvfile) + + if not reader.fieldnames: + print(f"Error: CSV file at {csv_filepath} appears to be empty or has no header row.") + return None + + # Check for required headers + if 'SourceDevice' not in reader.fieldnames or 'TargetDevice' not in reader.fieldnames: + print(f"Error: CSV file must contain 'SourceDevice' and 'TargetDevice' columns. Found: {reader.fieldnames}") + return None + + for row in reader: + source = row['SourceDevice'].strip() + target = row['TargetDevice'].strip() + + if not source or not target: + print(f"Warning: Skipping row with empty source or target device: {row}") + continue + + nodes.add(source) + nodes.add(target) + graph.add_edge(source, target) + + # Ensure all nodes mentioned (even if isolated after some links are skipped) are in the graph + # Networkx add_edge automatically adds nodes, but if a node was only mentioned + # in a row that had an empty counterpart, it might be missed. + # This explicit add ensures all devices from valid parts of rows are included. + for node in nodes: + if not graph.has_node(node): # Though add_edge should handle this if source/target were valid + graph.add_node(node) + + if not graph.nodes(): + print("Warning: No nodes found in the graph. The CSV might be empty or all rows were invalid.") + + return graph + + except FileNotFoundError: + print(f"Error: CSV file not found at {csv_filepath}") + return None + except Exception as e: + print(f"An error occurred while reading or parsing the CSV file: {e}") + return None + +def draw_network_graph(graph, output_filepath, layout_type='spring'): + """ + Draws the given networkx graph using matplotlib and saves it to a file. + + Args: + graph (nx.Graph): The networkx graph to draw. + output_filepath (str): The path to save the output PNG image. + layout_type (str): The layout algorithm to use ('spring', 'kamada_kawai', 'circular', 'random', 'shell', 'spectral'). + + Returns: + bool: True if drawing and saving was successful, False otherwise. + """ + if not graph or not graph.nodes(): + print("Graph is empty or has no nodes. Cannot draw.") + return False + + plt.figure(figsize=(12, 10)) # Adjust figure size as needed + + # Choose layout algorithm + if layout_type == 'spring': + pos = nx.spring_layout(graph, k=0.15, iterations=20) # k adjusts spacing, iterations for convergence + elif layout_type == 'kamada_kawai': + pos = nx.kamada_kawai_layout(graph) + elif layout_type == 'circular': + pos = nx.circular_layout(graph) + elif layout_type == 'random': + pos = nx.random_layout(graph) + elif layout_type == 'shell': + pos = nx.shell_layout(graph) + elif layout_type == 'spectral': + pos = nx.spectral_layout(graph) + else: + print(f"Warning: Unknown layout type '{layout_type}'. Defaulting to spring layout.") + pos = nx.spring_layout(graph, k=0.15, iterations=20) + + try: + nx.draw(graph, pos, + with_labels=True, + node_color='skyblue', + node_size=2000, # Increased node size + edge_color='gray', + font_size=10, # Adjusted font size + font_weight='bold', + width=1.5, # Edge width + alpha=0.9) # Node/edge transparency + + plt.title(f"Network Map ({layout_type.capitalize()} Layout)", size=15) + plt.savefig(output_filepath, format="PNG", dpi=150) # Save as PNG with decent DPI + plt.close() # Close the figure to free memory + return True + except Exception as e: + print(f"An error occurred while drawing or saving the graph: {e}") + plt.close() # Ensure figure is closed even if error occurs + return False + +def export_graph_gexf(graph, output_filepath): + """Exports the given networkx graph to GEXF format.""" + if not graph: # Check if graph is None or empty (no nodes might still be valid for an empty file) + print("Graph is None. Cannot export.") + return False + try: + nx.write_gexf(graph, output_filepath) + return True + except Exception as e: + print(f"An error occurred while exporting graph to GEXF: {e}") + return False + +def export_graph_graphml(graph, output_filepath): + """Exports the given networkx graph to GraphML format.""" + if not graph: + print("Graph is None. Cannot export.") + return False + try: + nx.write_graphml(graph, output_filepath, infer_numeric_types=True) # infer_numeric_types can be useful + return True + except Exception as e: + print(f"An error occurred while exporting graph to GraphML: {e}") + return False + +if __name__ == '__main__': + # Create a dummy CSV for testing + dummy_csv_content = """SourceDevice,TargetDevice +Router1,Switch1 +Router2,Switch1 +Router1,Router2 +FirewallA,Router1 +Switch1,Server1 +Switch1,Server2 +Router2,FirewallB +IsolatedDevice1, +,IsolatedDevice2 +FirewallA,FirewallA +""" + dummy_csv_filepath = "test_network_links.csv" + with open(dummy_csv_filepath, 'w', newline='', encoding='utf-8') as f: + f.write(dummy_csv_content) + + print(f"--- Testing with '{dummy_csv_filepath}' ---") + network_graph = build_graph_from_csv(dummy_csv_filepath) + + if network_graph: + print(f"Graph created successfully with {network_graph.number_of_nodes()} nodes and {network_graph.number_of_edges()} edges.") + print("Nodes:", list(network_graph.nodes())) + print("Edges:", list(network_graph.edges())) + + # --- Test PNG Generation --- + output_png_path = "test_network_map.png" + print(f"\n--- Attempting to generate PNG: {output_png_path} ---") + success_png = draw_network_graph(network_graph, output_png_path, layout_type='spring') + if success_png: + print(f"PNG generated: {output_png_path} (Please check this file visually if running locally)") + # In a typical environment, you'd open the file here or have the user do it. + # For automated testing, we primarily check if the function ran without error. + # To be more thorough in a local env, one might compare to a reference image or check file size. + os.remove(output_png_path) # Clean up test file + else: + print(f"PNG generation failed for {output_png_path}") + + output_png_path_kk = "test_network_map_kk.png" + print(f"\n--- Attempting to generate PNG with Kamada-Kawai layout: {output_png_path_kk} ---") + success_png_kk = draw_network_graph(network_graph, output_png_path_kk, layout_type='kamada_kawai') + if success_png_kk: + print(f"PNG generated: {output_png_path_kk}") + os.remove(output_png_path_kk) + else: + print(f"PNG generation failed for {output_png_path_kk}") + + + # Test case: Non-existent file + print("\n--- Testing with non-existent file ---") + non_existent_graph = build_graph_from_csv("non_existent.csv") + if non_existent_graph is None: + print("Correctly handled non-existent file.") # Export functions will also be skipped as graph is None + + # --- Test GEXF Export --- + output_gexf_path = "test_network_map.gexf" + print(f"\n--- Attempting to export GEXF: {output_gexf_path} ---") + if network_graph: # Only attempt if graph building was successful + success_gexf = export_graph_gexf(network_graph, output_gexf_path) + if success_gexf: + print(f"GEXF exported: {output_gexf_path} (Verify with Gephi or similar tool if running locally)") + os.remove(output_gexf_path) # Clean up + else: + print(f"GEXF export failed for {output_gexf_path}") + else: + print("Skipping GEXF export as graph building failed earlier.") + + # --- Test GraphML Export --- + output_graphml_path = "test_network_map.graphml" + print(f"\n--- Attempting to export GraphML: {output_graphml_path} ---") + if network_graph: # Only attempt if graph building was successful + success_graphml = export_graph_graphml(network_graph, output_graphml_path) + if success_graphml: + print(f"GraphML exported: {output_graphml_path} (Verify with Gephi or similar tool if running locally)") + os.remove(output_graphml_path) # Clean up + else: + print(f"GraphML export failed for {output_graphml_path}") + else: + print("Skipping GraphML export as graph building failed earlier.") + + # Test case: CSV with wrong headers + wrong_header_csv_content = "DeviceA,DeviceB\nRouterX,SwitchY" + wrong_header_filepath = "wrong_headers.csv" + with open(wrong_header_filepath, 'w', newline='', encoding='utf-8') as f: + f.write(wrong_header_csv_content) + print(f"\n--- Testing with '{wrong_header_filepath}' (wrong headers) ---") + wrong_header_graph = build_graph_from_csv(wrong_header_filepath) + if wrong_header_graph is None: + print("Correctly handled CSV with wrong headers.") + + # Test case: Empty CSV + empty_csv_content = "SourceDevice,TargetDevice\n" + empty_csv_filepath = "empty.csv" + with open(empty_csv_filepath, 'w', newline='', encoding='utf-8') as f: + f.write(empty_csv_content) + print(f"\n--- Testing with '{empty_csv_filepath}' (empty CSV) ---") + empty_graph = build_graph_from_csv(empty_csv_filepath) + if empty_graph and not empty_graph.nodes(): # Should create a graph, but it will be empty + print(f"Handled empty CSV. Nodes: {empty_graph.number_of_nodes()}, Edges: {empty_graph.number_of_edges()}") + + + # Clean up dummy files + import os + os.remove(dummy_csv_filepath) + os.remove(wrong_header_filepath) + os.remove(empty_csv_filepath) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b824864 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +networkx>=2.5 +matplotlib>=3.3 +scipy>=1.5 # Added for certain networkx layouts like kamada_kawai diff --git a/scam_detector/__init__.py b/scam_detector/__init__.py new file mode 100644 index 0000000..f97bd31 --- /dev/null +++ b/scam_detector/__init__.py @@ -0,0 +1,18 @@ +# This file makes 'scam_detector' a Python package. + +# Expose constants and potentially functions if needed by other modules directly +from .heuristics import ( + URGENCY_KEYWORDS, + SENSITIVE_INFO_KEYWORDS, + TOO_GOOD_TO_BE_TRUE_KEYWORDS, + GENERIC_GREETINGS, + TECH_SUPPORT_SCAM_KEYWORDS, + PAYMENT_KEYWORDS, + URL_PATTERN, + SUSPICIOUS_TLDS, + CRYPTO_ADDRESS_PATTERNS, + PHONE_NUMBER_PATTERN, + HEURISTIC_WEIGHTS +) + +from .analyzer import analyze_text_for_scams diff --git a/scam_detector/__pycache__/__init__.cpython-312.pyc b/scam_detector/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..f9c7c31 Binary files /dev/null and b/scam_detector/__pycache__/__init__.cpython-312.pyc differ diff --git a/scam_detector/__pycache__/analyzer.cpython-312.pyc b/scam_detector/__pycache__/analyzer.cpython-312.pyc new file mode 100644 index 0000000..9c2bc3f Binary files /dev/null and b/scam_detector/__pycache__/analyzer.cpython-312.pyc differ diff --git a/scam_detector/__pycache__/heuristics.cpython-312.pyc b/scam_detector/__pycache__/heuristics.cpython-312.pyc new file mode 100644 index 0000000..e30fd50 Binary files /dev/null and b/scam_detector/__pycache__/heuristics.cpython-312.pyc differ diff --git a/scam_detector/analyzer.py b/scam_detector/analyzer.py new file mode 100644 index 0000000..0a1e467 --- /dev/null +++ b/scam_detector/analyzer.py @@ -0,0 +1,211 @@ +import re +from urllib.parse import urlparse +from .heuristics import ( + URGENCY_KEYWORDS, + SENSITIVE_INFO_KEYWORDS, + TOO_GOOD_TO_BE_TRUE_KEYWORDS, + GENERIC_GREETINGS, + TECH_SUPPORT_SCAM_KEYWORDS, + PAYMENT_KEYWORDS, + URL_PATTERN, + SUSPICIOUS_TLDS, + CRYPTO_ADDRESS_PATTERNS, + PHONE_NUMBER_PATTERN, + HEURISTIC_WEIGHTS +) + +# Pre-compile a regex for suspicious TLDs for efficiency if used frequently +# This creates a pattern like: \.(xyz|top|loan|club|...)$ +# Ensure TLDs are escaped if they contain special regex characters (none in current list) +SUSPICIOUS_TLD_REGEX = re.compile(r"\.(" + "|".join(tld.lstrip('.') for tld in SUSPICIOUS_TLDS) + r")$", re.IGNORECASE) + +# Keywords that might appear in URLs that are suspicious (especially if not on a primary domain) +SUSPICIOUS_URL_PATH_KEYWORDS = ["login", "verify", "account", "secure", "update", "signin", "banking", "password"] + + +def analyze_text_for_scams(text_content): + """ + Analyzes a block of text content for various scam indicators. + + Args: + text_content (str): The text to analyze. + + Returns: + dict: A dictionary containing: + 'score' (float): An overall scam likelihood score. + 'indicators_found' (list): A list of strings describing found indicators. + 'urls_analyzed' (list): A list of dicts for each URL found and its analysis. + """ + if not text_content: + return {"score": 0.0, "indicators_found": [], "urls_analyzed": []} + + text_lower = text_content.lower() # For case-insensitive keyword matching + score = 0.0 + indicators_found = [] + urls_analyzed_details = [] + + # 1. Keyword-based checks + keyword_checks = { + "URGENCY": URGENCY_KEYWORDS, + "SENSITIVE_INFO": SENSITIVE_INFO_KEYWORDS, + "TOO_GOOD_TO_BE_TRUE": TOO_GOOD_TO_BE_TRUE_KEYWORDS, + "GENERIC_GREETING": GENERIC_GREETINGS, + "TECH_SUPPORT": TECH_SUPPORT_SCAM_KEYWORDS, + "PAYMENT_REQUEST": PAYMENT_KEYWORDS, + } + + for category, keywords in keyword_checks.items(): + for keyword in keywords: + if keyword in text_lower: + message = f"Presence of '{category.replace('_', ' ').title()}' keyword: '{keyword}'" + indicators_found.append(message) + score += HEURISTIC_WEIGHTS.get(category, 1.0) + # Optimization: could break after first keyword in category if only counting category once + # For now, sum weights for each keyword hit to emphasize multiple indicators. + + # 2. Regex-based checks + # URLs + found_urls = URL_PATTERN.findall(text_content) + for url_str in found_urls: + url_analysis = {"url": url_str, "is_suspicious": False, "reasons": []} + + parsed_url = None + try: + # Add scheme if missing for urlparse + if not url_str.startswith(('http://', 'https://', 'ftp://')): + temp_url_str_for_parse = 'http://' + url_str + else: + temp_url_str_for_parse = url_str + parsed_url = urlparse(temp_url_str_for_parse) + except Exception as e: + # print(f"Warning: Could not parse URL '{url_str}': {e}") + url_analysis["reasons"].append(f"Could not parse URL string.") + # Continue with regex checks on url_str itself if parsing fails + + # Check for suspicious TLDs + domain_to_check = parsed_url.hostname if parsed_url else url_str # Fallback to full string if parse failed + if domain_to_check and SUSPICIOUS_TLD_REGEX.search(domain_to_check): + reason = f"URL uses a potentially suspicious TLD (e.g., {SUSPICIOUS_TLD_REGEX.search(domain_to_check).group(0)})" + url_analysis["reasons"].append(reason) + url_analysis["is_suspicious"] = True + score += HEURISTIC_WEIGHTS.get("SUSPICIOUS_TLD", 1.0) + + # Check for suspicious keywords in URL path/query or domain itself + # (e.g. yourbank.com.suspicious.xyz/login or secure-payment-verify.com) + # This is a simple check; more advanced would involve checking against known legit domains. + for keyword in SUSPICIOUS_URL_PATH_KEYWORDS: + if keyword in url_str.lower(): # Check the whole URL string + # Avoid flagging legit sites like "myaccount.google.com" just for "account" + # This needs refinement: only flag if domain is not a known major one. + # For MVP, this check is broad. + is_known_major_domain = False + if parsed_url and parsed_url.hostname: + known_domains = ["google.com", "facebook.com", "amazon.com", "apple.com", "microsoft.com", "paypal.com"] # Example list + for kd in known_domains: + if parsed_url.hostname.endswith(kd): + is_known_major_domain = True + break + + if not is_known_major_domain: + reason = f"URL contains suspicious keyword: '{keyword}'" + url_analysis["reasons"].append(reason) + url_analysis["is_suspicious"] = True + score += HEURISTIC_WEIGHTS.get("SUSPICIOUS_URL_KEYWORD", 1.0) + break # Only count one such keyword per URL for now + + if url_analysis["is_suspicious"]: + indicators_found.append(f"Suspicious URL found: {url_str} (Reasons: {'; '.join(url_analysis['reasons'])})") + urls_analyzed_details.append(url_analysis) + + + # Crypto Addresses + for crypto_name, pattern in CRYPTO_ADDRESS_PATTERNS.items(): + if pattern.search(text_content): # Search original text, not lowercased, as patterns might be case-sensitive + message = f"Potential {crypto_name} cryptocurrency address found." + indicators_found.append(message) + score += HEURISTIC_WEIGHTS.get("CRYPTO_ADDRESS", 2.0) + + # Phone Numbers (Presence alone is not a strong indicator, context matters, which is hard for MVP) + # For MVP, we'll just note if one is found. The weighting is important here. + if PHONE_NUMBER_PATTERN.search(text_content): + message = "Phone number detected in text." + indicators_found.append(message) + score += HEURISTIC_WEIGHTS.get("PHONE_NUMBER_UNSOLICITED", 0.25) # Low weight + + # TODO: Add more heuristics like: + # - Grammar/spelling (complex, likely requires external library for good results) + # - Sense of urgency combined with financial request + # - Analysis of sender (if email headers were available) + + return { + "score": round(score, 2), + "indicators_found": indicators_found, + "urls_analyzed": urls_analyzed_details + } + +if __name__ == '__main__': + test_cases = [ + { + "name": "Phishing Attempt", + "text": "Dear Customer, your account is suspended due to unusual activity. Please verify your password at http://yourbank.secure-login-update.xyz/verify immediately. Act now to avoid closure.", + "expected_min_score": 5.0, # URGENCY, SENSITIVE_INFO, SUSPICIOUS_TLD, SUSPICIOUS_URL_KEYWORD + }, + { + "name": "Prize Scam", + "text": "CONGRATULATIONS YOU WON!!! You've won a free iPhone! Claim your reward now at www.totally-real-prize.top/claim-123. Provide your details to receive your prize.", + "expected_min_score": 4.0, # TOO_GOOD_TO_BE_TRUE, SENSITIVE_INFO, SUSPICIOUS_TLD + }, + { + "name": "Tech Support Scam", + "text": "Microsoft Support Alert: Your computer is infected with a virus! Call immediately 1-800-FAKE-TECH for a technician to get remote access. Your IP address compromised.", + "expected_min_score": 4.0, # TECH_SUPPORT, URGENCY, PHONE_NUMBER + }, + { + "name": "Crypto Payment Scam", + "text": "Urgent payment needed for outstanding invoice. Send 0.5 BTC to 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa to settle your account.", + "expected_min_score": 4.0, # URGENCY, PAYMENT_REQUEST, CRYPTO_ADDRESS + }, + { + "name": "Legitimate-sounding Message", + "text": "Hello John, just a reminder about our meeting tomorrow at 10 AM. Please find the agenda attached. Website: www.ourcompany.com. Call me if you have questions: (123) 456-7890", + "expected_max_score": 2.0, # Might pick up phone number, or URL if not whitelisted + }, + { + "name": "Generic Greeting Email", + "text": "Dear valued customer, We are updating our terms of service. No action needed from your side. Visit https://realcompany.com/terms for details.", + "expected_max_score": 1.0, # GENERIC_GREETING + }, + { + "name": "URL with suspicious keyword but known domain", + "text": "Please login to your account at https://myaccount.google.com/login-activity to check recent activity.", + "expected_max_score": 0.5, # Should not flag "login" or "account" heavily due to known domain + } + ] + + for case in test_cases: + print(f"\n--- Test Case: {case['name']} ---") + print(f"Text: \"{case['text'][:100]}...\"" if len(case['text']) > 100 else f"Text: \"{case['text']}\"") + results = analyze_text_for_scams(case['text']) + print(f"Score: {results['score']}") + print("Indicators:") + for indicator in results['indicators_found']: + print(f" - {indicator}") + if results['urls_analyzed']: + print("URLs Analyzed:") + for url_info in results['urls_analyzed']: + print(f" - URL: {url_info['url']}, Suspicious: {url_info['is_suspicious']}, Reasons: {url_info.get('reasons', [])}") + + if "expected_min_score" in case: + assert results['score'] >= case['expected_min_score'], f"Score {results['score']} was less than expected min {case['expected_min_score']}" + print(f"Assertion: Score >= {case['expected_min_score']} PASSED") + if "expected_max_score" in case: + assert results['score'] <= case['expected_max_score'], f"Score {results['score']} was more than expected max {case['expected_max_score']}" + print(f"Assertion: Score <= {case['expected_max_score']} PASSED") + + print("\n--- Test with empty text ---") + empty_results = analyze_text_for_scams("") + assert empty_results['score'] == 0.0 + assert not empty_results['indicators_found'] + print("Empty text test passed.") + + print("\nCore analysis engine tests completed.") diff --git a/scam_detector/heuristics.py b/scam_detector/heuristics.py new file mode 100644 index 0000000..d43c48c --- /dev/null +++ b/scam_detector/heuristics.py @@ -0,0 +1,163 @@ +import re + +# --- Keyword Lists (case-insensitive matching will be applied) --- + +# Keywords/phrases indicating urgency or pressure +URGENCY_KEYWORDS = [ + "urgent", "immediate action required", "act now", "limited time", + "account suspended", "account will be closed", "final warning", + "security alert", "unusual activity detected", "important notification", + "don't delay", "expires soon", "offer ends today", "last chance", + "your subscription will be cancelled", "payment declined" # Removed "action needed" +] + +# Keywords/phrases related to requests for sensitive information +SENSITIVE_INFO_KEYWORDS = [ + "verify your password", "confirm your password", "update your password", + "password", "username", "login details", "credentials", + "social security number", "ssn", + "bank account", "account number", "routing number", "credit card number", + "cvv", "pin number", "mother's maiden name", "security question", + "confirm your details", "update your information", "verify your account", + "provide your details", "personal information" +] + +# Keywords/phrases indicating too-good-to-be-true offers, prizes, etc. +TOO_GOOD_TO_BE_TRUE_KEYWORDS = [ + "you have won", "you've won", "congratulations you won", "winner", "prize", + "free gift", "claim your reward", "lottery", "sweepstakes", + "guaranteed", "risk-free", "earn money fast", "work from home easy", + "investment opportunity", "high return", "get rich quick", + "inheritance", " unclaimed funds", "nigerian prince" # Classic ones +] + +# Generic greetings/salutations that can be suspicious in unsolicited contexts +GENERIC_GREETINGS = [ + "dear customer", "dear user", "dear valued customer", "dear account holder", + "dear friend", "hello sir/madam", "greetings" + # Note: "Hello" or "Hi" by themselves are too common to be reliably suspicious +] + +# Keywords often found in tech support scams +TECH_SUPPORT_SCAM_KEYWORDS = [ + "microsoft support", "windows support", "apple support", + "virus detected", "malware found", "your computer is infected", + "call immediately", "technician", "remote access", "ip address compromised" +] + +# Keywords related to payment requests or financial transactions +PAYMENT_KEYWORDS = [ + "payment", "invoice", "bill", "outstanding balance", "transfer funds", + "wire transfer", "gift card", "cryptocurrency", "bitcoin", "western union", "moneygram", + "urgent payment needed", "settle your account" +] + + +# --- Regular Expression Patterns --- + +# Basic URL detection - this is simple and can be expanded +# It aims to find things that look like URLs. More sophisticated parsing will be needed +# if we want to break them down further or check TLDs more accurately here. +URL_PATTERN = re.compile( + r'(?:(?:https?|ftp):\/\/|www\.)' # http://, https://, ftp://, www. + r'(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#\/%=~_|$?!:,.])*' # Non-space chars in URL + r'(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[A-Z0-9+&@#\/%=~_|$])', # Last char + re.IGNORECASE +) + +# Suspicious Top-Level Domains (TLDs) - This list is not exhaustive! +# Scammers often use newer, cheaper, or less common TLDs. +SUSPICIOUS_TLDS = [ + '.xyz', '.top', '.loan', '.club', '.work', '.online', '.biz', '.info', + '.icu', '.gq', '.cf', '.tk', '.ml', # Often free TLDs abused + '.link', '.click', '.site', '.live', '.buzz', '.stream', '.download', + # Sometimes, very long TLDs can be suspicious if combined with other factors +] +# Regex to check if a URL ends with one of these TLDs +# (Needs to be used after extracting the domain from a URL) +# Example: r"\.(xyz|top|loan)$" - will be built dynamically in analyzer + +# Pattern for detecting strings that look like cryptocurrency addresses +CRYPTO_ADDRESS_PATTERNS = { + "BTC": re.compile(r'\b(1[a-km-zA-HJ-NP-Z1-9]{25,34}|3[a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-zA-HJ-NP-Z0-9]{25,90})\b'), + "ETH": re.compile(r'\b(0x[a-fA-F0-9]{40})\b'), + # Add more for other common cryptos like LTC, XMR if needed +} + +# Pattern for phone numbers (very generic, adjust for specific country needs if possible) +# This is a basic example and might catch non-phone numbers or miss some valid ones. +# It aims for sequences of 7-15 digits, possibly with spaces, hyphens, or parentheses. +PHONE_NUMBER_PATTERN = re.compile( + r'(\+?\d{1,3}[-.\s]?)?(\(?\d{2,4}\)?[-.\s]?)?(\d{3,4}[-.\s]?\d{3,4})' # Simplified + # r'(?:(?:\+|00)[1-9]\d{0,2}[-.\s]?)?(?:(?:\(\d{1,4}\)|\d{1,4})[-.\s]?)?(?:\d{1,4}[-.\s]?){1,4}\d{1,4}' +) + + +# --- Scoring Weights (Example - can be tuned) --- +# These weights can be used by the analyzer to calculate a scam score. +HEURISTIC_WEIGHTS = { + "URGENCY": 1.5, + "SENSITIVE_INFO": 2.5, + "TOO_GOOD_TO_BE_TRUE": 2.0, + "GENERIC_GREETING": 0.5, # Lower weight as it's a weaker indicator alone + "TECH_SUPPORT": 2.0, + "PAYMENT_REQUEST": 1.5, + "SUSPICIOUS_URL_KEYWORD": 1.0, # e.g., "login," "verify" in URL path with non-primary domain + "SUSPICIOUS_TLD": 2.0, + "CRYPTO_ADDRESS": 2.5, # Requesting crypto is often a scam indicator + "PHONE_NUMBER_UNSOLICITED": 1.0, # Presence of phone number in unsolicited mail could be for callback scam + # "GRAMMAR_SPELLING": 0.5 (If implemented) +} + + +if __name__ == '__main__': + print("--- Heuristic Definitions ---") + print(f"Loaded {len(URGENCY_KEYWORDS)} urgency keywords.") + print(f"Loaded {len(SENSITIVE_INFO_KEYWORDS)} sensitive info keywords.") + print(f"Loaded {len(TOO_GOOD_TO_BE_TRUE_KEYWORDS)} too-good-to-be-true keywords.") + print(f"Loaded {len(GENERIC_GREETINGS)} generic greetings.") + print(f"Loaded {len(TECH_SUPPORT_SCAM_KEYWORDS)} tech support scam keywords.") + print(f"Loaded {len(PAYMENT_KEYWORDS)} payment keywords.") + + print(f"\nURL Pattern: {URL_PATTERN.pattern}") + print(f"Suspicious TLDs example: {SUSPICIOUS_TLDS[:5]}") + + print("\nCrypto Address Patterns:") + for crypto, pattern in CRYPTO_ADDRESS_PATTERNS.items(): + print(f" {crypto}: {pattern.pattern}") + + print(f"\nPhone Number Pattern: {PHONE_NUMBER_PATTERN.pattern}") + + print("\nHeuristic Weights:") + for category, weight in HEURISTIC_WEIGHTS.items(): + print(f" {category}: {weight}") + + # Test URL pattern + test_text_with_urls = "Visit www.example.com or http://another-site.co.uk/path?query=1 and also https://test.xyz/secure" + found_urls = URL_PATTERN.findall(test_text_with_urls) + print(f"\nURLs found in test text: {found_urls}") + assert len(found_urls) == 3 + + # Test Crypto patterns + btc_text = "Send 1 BTC to 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa now!" + eth_text = "My address is 0x1234567890abcdef1234567890abcdef12345678" + no_crypto_text = "This is a normal message." + + assert CRYPTO_ADDRESS_PATTERNS["BTC"].search(btc_text) + assert CRYPTO_ADDRESS_PATTERNS["ETH"].search(eth_text) + assert not CRYPTO_ADDRESS_PATTERNS["BTC"].search(no_crypto_text) + print("Crypto address pattern tests passed.") + + # Test phone number pattern (basic) + phone_text_1 = "Call us at (123) 456-7890 for help." + phone_text_2 = "Our number is +44 20 7946 0958." + phone_text_3 = "Contact 1234567890." + no_phone_text = "No number here." + + assert PHONE_NUMBER_PATTERN.search(phone_text_1) + assert PHONE_NUMBER_PATTERN.search(phone_text_2) + assert PHONE_NUMBER_PATTERN.search(phone_text_3) + assert not PHONE_NUMBER_PATTERN.search(no_phone_text) + print("Phone number pattern tests passed (basic).") + + print("\nHeuristics module loaded and basic regex patterns tested.") diff --git a/scam_main.py b/scam_main.py new file mode 100644 index 0000000..2648967 --- /dev/null +++ b/scam_main.py @@ -0,0 +1,101 @@ +import argparse +import sys +from scam_detector.analyzer import analyze_text_for_scams + +def main(): + parser = argparse.ArgumentParser( + description="Text-based Scam Detection Tool. Analyzes input text for common scam indicators.", + epilog="Example: python scam_main.py --text \"Dear Customer, click http://suspicious.link/login to verify your account now!\"" + ) + + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + "-t", "--text", + help="Text content to analyze for scams." + ) + group.add_argument( + "-f", "--file", + help="Path to a plain text file to read content from." + ) + group.add_argument( + "--stdin", + action="store_true", + help="Read text content from standard input (e.g., via pipe)." + ) + + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Enable verbose output (shows detailed URL analysis if URLs are found)." + ) + + # Add a threshold argument for a simple alert + parser.add_argument( + "--threshold", + type=float, + default=5.0, # Default threshold, can be adjusted + help="Score threshold above which a 'High Risk' warning is displayed (default: 5.0)." + ) + + args = parser.parse_args() + + input_text = "" + if args.text: + input_text = args.text + elif args.file: + try: + with open(args.file, 'r', encoding='utf-8') as f: + input_text = f.read() + except FileNotFoundError: + print(f"Error: File not found at {args.file}") + sys.exit(1) + except Exception as e: + print(f"Error reading file {args.file}: {e}") + sys.exit(1) + elif args.stdin: + print("Reading from stdin. Press Ctrl+D (Linux/macOS) or Ctrl+Z then Enter (Windows) to end input.") + input_text = sys.stdin.read() + + if not input_text.strip(): + print("Error: No input text provided to analyze.") + sys.exit(1) + + print("\nAnalyzing text...") + results = analyze_text_for_scams(input_text) + + print("\n--- Scam Analysis Results ---") + print(f"Overall Scam Likelihood Score: {results['score']}") + + if results['score'] == 0.0 and not results['indicators_found']: + print("No specific scam indicators found in the text.") + elif results['score'] < args.threshold / 2 : # Example: low risk + print("Assessment: Low risk of being a scam based on heuristics.") + elif results['score'] < args.threshold: # Example: medium risk + print("Assessment: Medium risk. Some indicators suggest caution.") + else: # High risk + print(f"WARNING: High risk! Score exceeds threshold of {args.threshold}.") + print("This content has multiple indicators commonly found in scams.") + + if results['indicators_found']: + print("\nIndicators Found:") + for indicator in results['indicators_found']: + print(f" - {indicator}") + + if args.verbose and results['urls_analyzed']: + print("\nDetailed URL Analysis:") + for url_info in results['urls_analyzed']: + print(f" - URL: {url_info['url']}") + print(f" Suspicious: {url_info['is_suspicious']}") + if url_info['reasons']: + print(f" Reasons: {'; '.join(url_info['reasons'])}") + else: + print(f" Reasons: None") + elif results['urls_analyzed'] and not args.verbose: + print("\n(Run with --verbose to see detailed URL analysis if URLs were found)") + + + print("\nDisclaimer: This tool uses heuristic-based detection and is not foolproof.") + print("Always exercise caution and use your best judgment. Do not rely solely on this tool for security decisions.") + +if __name__ == "__main__": + main() diff --git a/simple_browser/__init__.py b/simple_browser/__init__.py new file mode 100644 index 0000000..9bca7db --- /dev/null +++ b/simple_browser/__init__.py @@ -0,0 +1,2 @@ +# This file makes 'simple_browser' a Python package. +# It can be left empty. diff --git a/simple_browser/browser.py b/simple_browser/browser.py new file mode 100644 index 0000000..2577df3 --- /dev/null +++ b/simple_browser/browser.py @@ -0,0 +1,150 @@ +import sys +from PyQt5.QtCore import QUrl, Qt +from PyQt5.QtWidgets import QApplication, QMainWindow, QToolBar, QLineEdit, QPushButton, QVBoxLayout, QWidget +from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEngineSettings + +class SimpleBrowser(QMainWindow): + def __init__(self): + super().__init__() + + # --- Basic Window Setup --- + self.setWindowTitle("Simple Browser - Loading...") + self.setGeometry(100, 100, 1024, 768) # x, y, width, height + + # --- Web Engine View --- + self.browser = QWebEngineView() + # Set a default blank page or simple HTML content initially + self.browser.setHtml(""" + + Blank Page + +

Enter a URL above to start browsing.

+ + + """) + self.browser.urlChanged.connect(self.update_url_bar) + self.browser.titleChanged.connect(self.update_window_title) + self.browser.loadFinished.connect(self.on_load_finished) + + # --- Toolbar for Navigation --- + self.toolbar = QToolBar("Main Toolbar") + self.toolbar.setMovable(False) # Keep it simple, not movable + self.addToolBar(self.toolbar) + + # Back Button + self.back_button = QPushButton("< Back") + self.back_button.clicked.connect(self.browser.back) + self.toolbar.addWidget(self.back_button) + + # Forward Button + self.forward_button = QPushButton("Forward >") + self.forward_button.clicked.connect(self.browser.forward) + self.toolbar.addWidget(self.forward_button) + + # Reload Button + self.reload_button = QPushButton("Reload") + self.reload_button.clicked.connect(self.browser.reload) + self.toolbar.addWidget(self.reload_button) + + # Home Button (simple placeholder action for now) + self.home_button = QPushButton("Home") + self.home_button.clicked.connect(self.navigate_home) + self.toolbar.addWidget(self.home_button) + + # URL Bar (QLineEdit) + self.url_bar = QLineEdit() + self.url_bar.setPlaceholderText("Enter URL and press Enter") + self.url_bar.returnPressed.connect(self.navigate_to_url) + self.toolbar.addWidget(self.url_bar) + + + # --- Central Widget and Layout --- + # QWebEngineView will be the central widget + self.setCentralWidget(self.browser) + + # Initial button states + self.update_nav_buttons_state() + + + def navigate_to_url(self): + """Navigates to the URL entered in the URL bar.""" + url_text = self.url_bar.text().strip() + if not url_text: + return + + # Add http:// prefix if missing for convenience + if not url_text.startswith(('http://', 'https://')): + url_text = 'http://' + url_text + + self.browser.setUrl(QUrl(url_text)) + + def navigate_home(self): + """Navigates to a predefined home page (blank for now).""" + self.browser.setHtml(""" + + Blank Page + +

Welcome! Enter a URL above to start browsing.

+ + + """) + self.url_bar.setText("") # Clear URL bar for home + + def update_url_bar(self, qurl): + """Updates the URL bar with the current URL of the browser view.""" + # Only update if it's different, to avoid cursor jumping during typing + if qurl.toString() != self.url_bar.text(): + self.url_bar.setText(qurl.toString()) + + def update_window_title(self, title): + """Updates the main window title.""" + if title: + self.setWindowTitle(f"{title} - Simple Browser") + else: + self.setWindowTitle("Simple Browser") + + def on_load_finished(self, success): + """Called when a page load is finished.""" + self.update_nav_buttons_state() + if not success: + # Basic error page + self.browser.setHtml(f""" + Error Loading Page + +

Error Loading Page

+

Could not load the requested page: {self.url_bar.text()}

+

Please check the URL and your internet connection.

+ + """) + + def update_nav_buttons_state(self): + """Enables/disables back/forward buttons based on browser history.""" + self.back_button.setEnabled(self.browser.history().canGoBack()) + self.forward_button.setEnabled(self.browser.history().canGoForward()) + + +if __name__ == '__main__': + # --- Application Setup --- + # Enable High DPI scaling for better visuals on some systems + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + + app = QApplication(sys.argv) + + # --- Configure WebEngine Profile & Settings for Distraction-Free --- + # Get the default profile + profile = QWebEngineProfile.defaultProfile() + + # Attempt to disable JavaScript opening new windows (pop-ups) + # These settings are hints; behavior can vary based on web content. + settings = profile.settings() + settings.setAttribute(QWebEngineSettings.JavascriptCanOpenWindows, False) # Key for pop-ups + settings.setAttribute(QWebEngineSettings.PluginsEnabled, False) # Disable plugins like Flash (mostly obsolete but good for minimal) + settings.setAttribute(QWebEngineSettings.FullScreenSupportEnabled, True) # Allow full screen if page requests + # settings.setAttribute(QWebEngineSettings.AutoLoadImages, True) # Keep images by default + # settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True) # Keep JS for modern sites + + window = SimpleBrowser() + window.show() + + sys.exit(app.exec_()) diff --git a/wifi_analyzer/__init__.py b/wifi_analyzer/__init__.py new file mode 100644 index 0000000..e0368b1 --- /dev/null +++ b/wifi_analyzer/__init__.py @@ -0,0 +1,4 @@ +# This file makes 'wifi_analyzer' a Python package. +from .scanner import scan_wifi_networks, get_wifi_interface +from .analyzer import get_channel_usage, display_channel_usage_text +from .monitor import get_current_connection_info_nmcli, get_current_signal_strength_iw, monitor_current_connection_signal diff --git a/wifi_analyzer/analyzer.py b/wifi_analyzer/analyzer.py new file mode 100644 index 0000000..44b91f8 --- /dev/null +++ b/wifi_analyzer/analyzer.py @@ -0,0 +1,138 @@ +from collections import Counter + +def get_channel_usage(networks_data): + """ + Analyzes scanned Wi-Fi networks to determine channel usage. + + Args: + networks_data (list): A list of dictionaries, where each dictionary + represents a Wi-Fi network and must contain + 'channel' and 'band' keys. + + Returns: + tuple: A tuple containing two dictionaries: + - channel_usage_2_4ghz (dict): Channel counts for 2.4 GHz band. + - channel_usage_5ghz (dict): Channel counts for 5 GHz band. + """ + channels_2_4ghz = [] + channels_5ghz = [] + + for net in networks_data: + channel = net.get('channel') + band = net.get('band') + + if not channel or not band: # Skip if essential data is missing + continue + + if band == "2.4 GHz": + channels_2_4ghz.append(channel) + elif band == "5 GHz": + channels_5ghz.append(channel) + + # Count occurrences of each channel + channel_usage_2_4ghz = Counter(channels_2_4ghz) + channel_usage_5ghz = Counter(channels_5ghz) + + return dict(sorted(channel_usage_2_4ghz.items())), dict(sorted(channel_usage_5ghz.items())) + +def display_channel_usage_text(channel_usage_2_4ghz, channel_usage_5ghz): + """ + Displays channel usage information in a text-based format. + Includes a simple text-based bar graph for 2.4 GHz. + """ + print("\n--- Wi-Fi Channel Usage ---") + + if not channel_usage_2_4ghz and not channel_usage_5ghz: + print("No channel data to display.") + return + + if channel_usage_2_4ghz: + print("\n[2.4 GHz Band]") + max_count_2_4 = 0 + if channel_usage_2_4ghz.values(): # Check if there are any counts + max_count_2_4 = max(channel_usage_2_4ghz.values(), default=0) + + # Determine max bar length for scaling the simple text graph + # Max bar length can be, e.g., 50 characters + max_bar_len = 40 + + for channel in range(1, 15): # Common 2.4 GHz channels (1-14) + count = channel_usage_2_4ghz.get(channel, 0) + if count > 0 : # Only print channels in use or if we want to show all, then remove this if + bar = "" + if max_count_2_4 > 0: # Avoid division by zero + bar_len = int((count / max_count_2_4) * max_bar_len) if max_count_2_4 > 0 else 0 + bar = '█' * bar_len + print(f"Channel {channel:2d}: {count:2d} network(s) {bar}") + elif channel_usage_2_4ghz and channel in channel_usage_2_4ghz: # Explicitly show if 0 but was in data + print(f"Channel {channel:2d}: {count:2d} network(s)") + + + if channel_usage_5ghz: + print("\n[5 GHz Band]") + # For 5GHz, channels are numerous and less prone to overlap in the same way. + # A simple list format is probably clearer than a text bar graph covering all possible channels. + for channel, count in channel_usage_5ghz.items(): + print(f"Channel {channel:3d}: {count:2d} network(s)") + + print("\nNote: Channel overlap in the 2.4 GHz band means networks on adjacent channels can interfere.") + print("Aim for channels 1, 6, or 11 where possible in crowded 2.4 GHz environments.") + + +if __name__ == '__main__': + # Mock data for testing get_channel_usage and display + mock_networks = [ + {'ssid': 'NetA', 'signal': 70, 'channel': 1, 'band': '2.4 GHz', 'security': 'WPA2'}, + {'ssid': 'NetB', 'signal': 60, 'channel': 1, 'band': '2.4 GHz', 'security': 'WPA2'}, + {'ssid': 'NetC', 'signal': 80, 'channel': 6, 'band': '2.4 GHz', 'security': 'WPA2'}, + {'ssid': 'NetD', 'signal': 50, 'channel': 6, 'band': '2.4 GHz', 'security': 'WPA2'}, + {'ssid': 'NetE', 'signal': 75, 'channel': 6, 'band': '2.4 GHz', 'security': 'WPA2'}, + {'ssid': 'NetF', 'signal': 65, 'channel': 11, 'band': '2.4 GHz', 'security': 'WPA2'}, + {'ssid': 'NetG', 'signal': 85, 'channel': 36, 'band': '5 GHz', 'security': 'WPA2'}, + {'ssid': 'NetH', 'signal': 90, 'channel': 36, 'band': '5 GHz', 'security': 'WPA2'}, + {'ssid': 'NetI', 'signal': 70, 'channel': 40, 'band': '5 GHz', 'security': 'WPA2'}, + {'ssid': 'NetJ', 'signal': 60, 'channel': 149, 'band': '5 GHz', 'security': 'WPA2'}, + {'ssid': 'NetK', 'signal': 50, 'channel': 14, 'band': '2.4 GHz', 'security': 'WPA2'}, # Channel 14 (Japan) + {'ssid': 'NetL', 'signal': 0, 'channel': 0, 'band': 'Unknown', 'security': 'WPA2'}, # Invalid data + {'ssid': 'NetM', 'signal': 50, 'channel': 6, 'band': '2.4 GHz', 'security': 'WPA2'}, + ] + + usage_2_4, usage_5 = get_channel_usage(mock_networks) + + print("--- Mock Data Test ---") + print("2.4 GHz Usage:", usage_2_4) + print("5 GHz Usage:", usage_5) + + assert usage_2_4.get(1) == 2 + assert usage_2_4.get(6) == 4 + assert usage_2_4.get(11) == 1 + assert usage_2_4.get(14) == 1 + assert usage_5.get(36) == 2 + assert usage_5.get(40) == 1 + assert usage_5.get(149) == 1 + print("get_channel_usage assertions passed.") + + display_channel_usage_text(usage_2_4, usage_5) + + print("\n--- Test with Empty Data ---") + empty_usage_2_4, empty_usage_5 = get_channel_usage([]) + display_channel_usage_text(empty_usage_2_4, empty_usage_5) + assert not empty_usage_2_4 + assert not empty_usage_5 + print("Empty data test passed.") + + print("\n--- Test with Only 2.4 GHz Data ---") + only_2_4_data = [net for net in mock_networks if net['band'] == '2.4 GHz'] + usage_2_4_only, usage_5_only = get_channel_usage(only_2_4_data) + display_channel_usage_text(usage_2_4_only, usage_5_only) + assert usage_2_4_only + assert not usage_5_only + print("Only 2.4 GHz data test passed.") + + print("\n--- Test with Only 5 GHz Data ---") + only_5_data = [net for net in mock_networks if net['band'] == '5 GHz'] + usage_2_4_only_5, usage_5_only_5 = get_channel_usage(only_5_data) + display_channel_usage_text(usage_2_4_only_5, usage_5_only_5) + assert not usage_2_4_only_5 + assert usage_5_only_5 + print("Only 5 GHz data test passed.") diff --git a/wifi_analyzer/monitor.py b/wifi_analyzer/monitor.py new file mode 100644 index 0000000..2200c80 --- /dev/null +++ b/wifi_analyzer/monitor.py @@ -0,0 +1,250 @@ +import subprocess +import time +import re + +def get_current_connection_info_nmcli(interface): + """ + Gets current connection details using 'nmcli dev show '. + Parses for SSID and signal strength. + """ + try: + cmd = ['nmcli', '-t', '-f', 'GENERAL.CONNECTION,WIFI.SSID,WIFI.SIGNAL', 'dev', 'show', interface] + result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=5) + + ssid = "N/A" + signal = 0 + connection_name = "N/A" + + for line in result.stdout.strip().split('\n'): + if line.startswith("GENERAL.CONNECTION:"): + connection_name = line.split(':', 1)[1].replace('\\:', ':') + elif line.startswith("WIFI.SSID:") or line.startswith("802-11-wireless.ssid:"): # Some versions use different prefix + ssid = line.split(':', 1)[1].replace('\\:', ':') + elif line.startswith("WIFI.SIGNAL:") or line.startswith("802-11-wireless.signal:"): + signal_str = line.split(':', 1)[1] + if signal_str.isdigit(): + signal = int(signal_str) + + if connection_name == "--" or not connection_name : # '--' means not connected or no active connection name + if ssid == "--" or not ssid: # Double check if SSID was also empty or '--' + return None # Not connected or no info + + # If SSID is still empty but connection name exists, it might be a non-Wi-Fi connection or nmcli version difference + # For Wi-Fi, SSID should generally be present if connected. + if not ssid or ssid == "--": # If SSID is explicitly "--" or empty, treat as not connected to Wi-Fi for our purpose + return None + + return {"ssid": ssid, "signal": signal, "connection_name": connection_name} + + except FileNotFoundError: + print("Error: nmcli command not found for current connection info.") + return None + except subprocess.CalledProcessError: + # This can happen if the interface doesn't exist or isn't Wi-Fi, or other nmcli errors + # print(f"Error getting connection info for {interface} via nmcli: {e.stderr}") + return None # Indicates not connected or error + except subprocess.TimeoutExpired: + print(f"Timeout getting connection info for {interface} via nmcli.") + return None + except Exception as e: + print(f"An unexpected error occurred while getting current connection info with nmcli: {e}") + return None + +def get_current_signal_strength_iw(interface): + """ + Gets current signal strength using 'iw dev link'. + Parses for SSID and signal strength (dBm). + Note: This usually requires sudo if not already granted for iw. + However, `iw dev link` often works without sudo for current link. + """ + try: + cmd = ['iw', 'dev', interface, 'link'] + result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=5) + + ssid = "N/A" + signal_dbm = None # Typically a negative value like -50 + + lines = result.stdout.strip().split('\n') + if lines[0].strip().startswith(f"Not connected."): # Check first line for "Not connected." + return None + + for line in lines: + if line.strip().startswith("SSID:"): + ssid = line.split("SSID:", 1)[1].strip() + elif "signal:" in line: + match = re.search(r"signal:\s*(-?\d+)\s*dBm", line) + if match: + signal_dbm = int(match.group(1)) + + if ssid == "N/A" and signal_dbm is None: # If neither was found, likely not connected + return None + + return {"ssid": ssid, "signal_dbm": signal_dbm} + + except FileNotFoundError: + print("Error: iw command not found for current signal strength.") + return None + except subprocess.CalledProcessError: + # This can happen if the interface is not connected or other iw errors + # print(f"Error getting signal strength for {interface} via iw: {e.stderr}") + return None + except subprocess.TimeoutExpired: + print(f"Timeout getting signal strength for {interface} via iw.") + return None + except Exception as e: + print(f"An unexpected error occurred while getting current signal strength with iw: {e}") + return None + + +def monitor_current_connection_signal(interface, duration=10, interval=2, use_iw=False): + """ + Monitors and displays the signal strength of the current Wi-Fi connection. + + Args: + interface (str): The Wi-Fi interface name. + duration (int): How long to monitor in seconds. + interval (int): How often to check the signal in seconds. + use_iw (bool): If True, try to use 'iw' for signal strength (dBm). Otherwise, use 'nmcli'. + """ + print(f"\n--- Monitoring Signal Strength for Current Connection on '{interface}' ---") + print(f"Monitoring for {duration} seconds (updates every {interval}s). Press Ctrl+C to stop early.") + + end_time = time.time() + duration + try: + while time.time() < end_time: + info = None + if use_iw: + info = get_current_signal_strength_iw(interface) + if info: + print(f"SSID: {info['ssid']}, Signal: {info['signal_dbm']} dBm") + else: + print("Not connected or unable to fetch signal via iw.") + else: # use nmcli + info = get_current_connection_info_nmcli(interface) + if info: + print(f"SSID: {info['ssid']}, Signal Quality: {info['signal']}% (Connection: {info['connection_name']})") + else: + print("Not connected or unable to fetch signal via nmcli.") + + # Wait for the next interval, but check frequently for Ctrl+C + for _ in range(interval * 10): # Check 10 times per second for interval + if time.time() >= end_time: + break + time.sleep(0.1) + if time.time() >= end_time: + break + except KeyboardInterrupt: + print("\nMonitoring stopped by user.") + except Exception as e: + print(f"\nAn error occurred during monitoring: {e}") + finally: + print("Monitoring finished.") + + +if __name__ == '__main__': + # This import is only for the __main__ block to reuse get_wifi_interface + from .scanner import get_wifi_interface + + print("Attempting to find Wi-Fi interface for monitoring...") + wifi_interface = get_wifi_interface() + + if wifi_interface: + print(f"Using Wi-Fi interface: {wifi_interface}") + + print("\n--- Testing nmcli for current connection info ---") + nmcli_info = get_current_connection_info_nmcli(wifi_interface) + if nmcli_info: + print(f"nmcli - SSID: {nmcli_info['ssid']}, Signal: {nmcli_info['signal']}%, Connection: {nmcli_info['connection_name']}") + else: + print("nmcli - Not connected or no info retrieved.") + + print("\n--- Testing iw for current connection info ---") + iw_info = get_current_signal_strength_iw(wifi_interface) + if iw_info: + print(f"iw - SSID: {iw_info['ssid']}, Signal: {iw_info['signal_dbm']} dBm") + else: + print("iw - Not connected or no info retrieved.") + + # Example of running the monitor + # Choose one method for the actual monitoring call, or allow user to choose in a real CLI + # For testing, we'll prefer nmcli if available as it doesn't usually need sudo. + + # To actually run monitoring for a short period: + # print("\n--- Starting live monitor test (nmcli) ---") + # monitor_current_connection_signal(wifi_interface, duration=6, interval=2, use_iw=False) + + # print("\n--- Starting live monitor test (iw) ---") + # monitor_current_connection_signal(wifi_interface, duration=6, interval=2, use_iw=True) + + else: + print("Could not run monitoring tests as no Wi-Fi interface was found.") + + print("\n--- Mock Test: nmcli parsing ---") + mock_nmcli_output_connected = """GENERAL.CONNECTION:MyHomeWiFi +WIFI.SSID:MyHomeWiFi +WIFI.SIGNAL:85 +""" + mock_nmcli_output_not_connected = "GENERAL.CONNECTION:--\nWIFI.SSID:--\nWIFI.SIGNAL:0\n" + # Simulate subprocess run for parsing test + class MockCompletedProcess: + def __init__(self, stdout, stderr="", returncode=0): + self.stdout = stdout + self.stderr = stderr + self.returncode = returncode + def check_returncode(self): + if self.returncode != 0: + raise subprocess.CalledProcessError(self.returncode, "cmd", output=self.stdout, stderr=self.stderr) + + original_subprocess_run = subprocess.run # Save original + + # Mock for connected state + def mock_run_connected(*args, **kwargs): + return MockCompletedProcess(mock_nmcli_output_connected) + subprocess.run = mock_run_connected + parsed_info = get_current_connection_info_nmcli("wlan_mock") + assert parsed_info is not None + assert parsed_info["ssid"] == "MyHomeWiFi" + assert parsed_info["signal"] == 85 + print("Mock nmcli (connected) parsing: PASS") + + # Mock for not connected state + def mock_run_not_connected(*args, **kwargs): + return MockCompletedProcess(mock_nmcli_output_not_connected) + subprocess.run = mock_run_not_connected + parsed_info_nc = get_current_connection_info_nmcli("wlan_mock_nc") + assert parsed_info_nc is None + print("Mock nmcli (not connected) parsing: PASS") + + subprocess.run = original_subprocess_run # Restore original + + print("\n--- Mock Test: iw parsing ---") + mock_iw_output_connected = """Connected to aa:bb:cc:dd:ee:ff (on wlan_mock) + SSID: MyIWNetwork + freq: 2412 + RX: 12345 bytes (100 packets) + TX: 6789 bytes (50 packets) + signal: -55 dBm + tx bitrate: 72.2 MBit/s +""" + mock_iw_output_not_connected = "Not connected." + + # Mock for iw connected + def mock_run_iw_connected(*args, **kwargs): + return MockCompletedProcess(mock_iw_output_connected) + subprocess.run = mock_run_iw_connected + parsed_iw_info = get_current_signal_strength_iw("wlan_mock_iw") + assert parsed_iw_info is not None + assert parsed_iw_info["ssid"] == "MyIWNetwork" + assert parsed_iw_info["signal_dbm"] == -55 + print("Mock iw (connected) parsing: PASS") + + # Mock for iw not connected + def mock_run_iw_not_connected(*args, **kwargs): + return MockCompletedProcess(mock_iw_output_not_connected) + subprocess.run = mock_run_iw_not_connected + parsed_iw_info_nc = get_current_signal_strength_iw("wlan_mock_iw_nc") + assert parsed_iw_info_nc is None + print("Mock iw (not connected) parsing: PASS") + + subprocess.run = original_subprocess_run # Restore original +``` diff --git a/wifi_analyzer/scanner.py b/wifi_analyzer/scanner.py new file mode 100644 index 0000000..b022307 --- /dev/null +++ b/wifi_analyzer/scanner.py @@ -0,0 +1,236 @@ +import subprocess +import re + +def get_wifi_interface(): + """ + Tries to automatically determine the active Wi-Fi interface name on Linux. + This is a helper and might not be foolproof on all systems. + """ + try: + # Try 'iw dev' first, as it's more modern + result = subprocess.run(['iw', 'dev'], capture_output=True, text=True, check=True) + for line in result.stdout.splitlines(): + if line.strip().startswith('Interface '): + interface = line.strip().split(' ')[1] + # Check if it's a wlan-type interface + if interface.startswith(('wlan', 'wlp', 'wlx', 'ath')): # Common Wi-Fi interface prefixes + return interface + except (FileNotFoundError, subprocess.CalledProcessError): + pass # iw not found or error, try nmcli + + try: + # Fallback to 'nmcli dev status' + result = subprocess.run(['nmcli', '-t', '-f', 'DEVICE,TYPE', 'dev', 'status'], capture_output=True, text=True, check=True) + for line in result.stdout.splitlines(): + parts = line.strip().split(':') + if len(parts) == 2 and parts[1] == 'wifi': + return parts[0] # Device name + except (FileNotFoundError, subprocess.CalledProcessError): + print("Error: Could not automatically determine Wi-Fi interface using 'iw dev' or 'nmcli'.") + print("Please ensure 'iw' or 'nmcli' (NetworkManager) is installed and in your PATH.") + return None + + +def parse_nmcli_output(output_str): + """ + Parses the output of `nmcli -f SSID,CHAN,FREQ,SIGNAL,SECURITY dev wifi list`. + """ + networks = [] + lines = output_str.strip().split('\n') + + if not lines or len(lines) < 1: # Header might be missing if no networks + return networks + + header = lines[0].strip() # Potential header like "SSID CHAN FREQ SIGNAL SECURITY" + # Actual data lines start from index 1 if header is present, or 0 if no networks found (nmcli might just output nothing or only header) + + # Determine column indices dynamically - more robust but assumes consistent field names + # For simplicity in MVP, we'll assume a fixed order if a simple `nmcli dev wifi list` is used, + # or rely on the exact fields if `-f` is used. + # Let's assume we use `nmcli -f SSID,CHAN,FREQ,SIGNAL,SECURITY dev wifi list` + # which should give a predictable headerless output if there's data, or just header if no data. + # If `nmcli` outputs a header even with `-f` on some versions, we need to skip it. + + data_lines = lines + if "SSID" in header and "CHAN" in header: # Check if the first line is a header + data_lines = lines[1:] + + for line in data_lines: + line = line.strip() + if not line: + continue + + # Example line with -f SSID,CHAN,FREQ,SIGNAL,SECURITY: + # MyWiFi:6:2437 MHz:80:WPA2 + # Sometimes nmcli uses variable spaces as delimiters if not using -t (terse) + # Using -t with -f helps: `nmcli -t -f SSID,CHAN,FREQ,SIGNAL,SECURITY dev wifi list` + # Output: MyWiFi\:Other:6:2437 MHz:75:WPA2 (SSIDs with colons are backslash-escaped) + + # We will assume the output of `nmcli -t -f ...` which uses ':' as a separator. + # SSIDs can contain escaped colons, so simple split by ':' is not enough. + # A more robust way is to use regex if we stick to non-terse, or handle escaped colons if terse. + # For now, let's try a regex that handles spaces and then refine if needed. + # Target fields: SSID, CHAN, FREQ, SIGNAL, SECURITY + + # Regex for parsing `nmcli -f SSID,CHAN,FREQ,SIGNAL,SECURITY dev wifi list` (non-terse) + # This regex expects at least two spaces between fields, which is common for nmcli's default output. + # It tries to capture SSID greedily, then looks for subsequent fields. + # Example: "My Great SSID 6 2437 MHz 78 WPA2" + # Example: "AnotherNet 11 2462 MHz 60 WPA1 WPA2" (Security can have spaces) + + # Let's try a simpler parsing first, assuming `nmcli -t -f SSID,CHAN,FREQ,SIGNAL,SECURITY dev wifi list` + # which should give colon-separated values. SSIDs with ':' will be `\:`. + parts = line.split(':') + if len(parts) >= 5: # SSID, CHAN, FREQ, SIGNAL, SECURITY[, EXTRA_STUFF_IF_ANY] + ssid = parts[0].replace('\\:', ':') # Handle escaped colons in SSID + channel_str = parts[1] + freq_str = parts[2] # e.g., "2437 MHz" or "5180 MHz" + signal_str = parts[3] # e.g., "80" + security = parts[4] + # If security field itself contains colons due to multiple methods, join remaining parts + if len(parts) > 5: + security = ":".join(parts[4:]) + + + try: + channel = int(channel_str) if channel_str else 0 + signal = int(signal_str) if signal_str else 0 + + band = "Unknown" + if "MHz" in freq_str: + freq_mhz = int(freq_str.split(" ")[0]) + if 2400 <= freq_mhz < 2500: # 2.4 GHz band typically 2412-2484 MHz + band = "2.4 GHz" + elif 5100 <= freq_mhz < 5900: # 5 GHz band typically 5170-5825 MHz + band = "5 GHz" + + networks.append({ + "ssid": ssid, + "signal": signal, + "channel": channel, + "band": band, + "frequency": freq_str, + "security": security if security else "Open" # Assume open if security is empty + }) + except ValueError: + print(f"Warning: Could not parse numeric value in line: {line}") + continue + # else: + # print(f"Warning: Could not parse line, expected at least 5 colon-separated parts: '{line}'") + + return networks + +def scan_wifi_networks(interface=None): + """ + Scans for available Wi-Fi networks using nmcli on Linux. + + Args: + interface (str, optional): The Wi-Fi interface name. If None, tries to auto-detect. + + Returns: + list: A list of dictionaries, where each dictionary represents a Wi-Fi network. + Returns an empty list if scanning fails or no networks are found. + """ + if interface is None: + interface = get_wifi_interface() + if interface is None: + print("Exiting: No Wi-Fi interface provided or auto-detected.") + return [] + + # Use -t for terse output (colon-separated), -f for specific fields. + # Rescan to ensure fresh results. Sometimes `nmcli dev wifi rescan` is needed first. + try: + subprocess.run(['nmcli', 'dev', 'wifi', 'rescan', 'ifname', interface], + capture_output=True, text=True, timeout=10) # Allow some time for rescan + except FileNotFoundError: + print("Error: nmcli command not found. Please ensure NetworkManager is installed.") + return [] + except subprocess.TimeoutExpired: + print("Warning: nmcli rescan command timed out. Proceeding with potentially stale data.") + except subprocess.CalledProcessError as e: + print(f"Warning: nmcli rescan failed: {e.stderr}") + # Continue anyway, list might still work with cached data + + cmd = ['nmcli', '-t', '-f', 'SSID,CHAN,FREQ,SIGNAL,SECURITY', 'dev', 'wifi', 'list', 'ifname', interface] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=15) + if result.stderr: + print(f"Warning/Error from nmcli list: {result.stderr.strip()}") + return parse_nmcli_output(result.stdout) + except FileNotFoundError: + print("Error: nmcli command not found. Please ensure NetworkManager is installed.") + return [] + except subprocess.CalledProcessError as e: + print(f"Error scanning Wi-Fi networks with nmcli: {e}") + print(f"STDERR: {e.stderr.strip()}") + return [] + except subprocess.TimeoutExpired: + print("Error: nmcli list command timed out.") + return [] + except Exception as e: + print(f"An unexpected error occurred during Wi-Fi scan: {e}") + return [] + +if __name__ == '__main__': + print("Attempting to find Wi-Fi interface...") + wifi_interface = get_wifi_interface() + + if wifi_interface: + print(f"Using Wi-Fi interface: {wifi_interface}") + print("\nScanning for Wi-Fi networks...") + networks = scan_wifi_networks(interface=wifi_interface) + if networks: + print(f"\nFound {len(networks)} networks:") + for net in networks: + print(f" SSID: {net['ssid']}") + print(f" Signal: {net['signal']}%") + print(f" Channel: {net['channel']} ({net['band']})") + print(f" Frequency: {net['frequency']}") + print(f" Security: {net['security']}") + print("-" * 20) + else: + print("No networks found or error during scan.") + else: + print("Could not run scan as no Wi-Fi interface was found.") + + print("\n--- Test parsing with mock nmcli output ---") + mock_output_header = """SSID:CHAN:FREQ:SIGNAL:SECURITY +MyAwesomeWiFi:11:2462 MHz:88:WPA2 +AnotherNet\:Work:6:2437 MHz:70:WPA1 WPA2 +Open unsecured:1:2412 MHz:50:-- +WEPNet:48:5240 MHz:62:WEP +""" + mock_output_no_header = """MyAwesomeWiFi:11:2462 MHz:88:WPA2 +AnotherNet\:Work:6:2437 MHz:70:WPA1 WPA2 +Open unsecured:1:2412 MHz:50: +WEPNet:48:5240 MHz:62:WEP +EmptySignal::2412 MHz::WPA2 +""" # nmcli -t -f ... does not print header if there is data + + parsed_networks_mock = parse_nmcli_output(mock_output_no_header) + print(f"Parsed {len(parsed_networks_mock)} networks from mock data:") + for net in parsed_networks_mock: + print(f" SSID: {net['ssid']}, Signal: {net['signal']}, Chan: {net['channel']}, Band: {net['band']}, Security: {net['security']}") + + assert len(parsed_networks_mock) == 4 # "EmptySignal" fails int conversion for signal + assert parsed_networks_mock[0]['ssid'] == "MyAwesomeWiFi" + assert parsed_networks_mock[0]['signal'] == 88 + assert parsed_networks_mock[1]['ssid'] == "AnotherNet:Work" + assert parsed_networks_mock[1]['security'] == "WPA1 WPA2" + assert parsed_networks_mock[2]['security'] == "Open" + assert parsed_networks_mock[3]['band'] == "5 GHz" + print("Mock data parsing tests passed (basic checks).") + + # Test with header (nmcli might output header if no networks are found) + parsed_with_header = parse_nmcli_output("SSID:CHAN:FREQ:SIGNAL:SECURITY\n") + assert len(parsed_with_header) == 0 + print("Parsing empty data with only header line passed.") + + parsed_no_networks = parse_nmcli_output("SSID:CHAN:FREQ:SIGNAL:SECURITY") # Only header + assert len(parsed_no_networks) == 0 + print("Parsing only header line (no newline) passed.") + + parsed_empty_str = parse_nmcli_output("") + assert len(parsed_empty_str) == 0 + print("Parsing empty string passed.") diff --git a/wifi_main.py b/wifi_main.py new file mode 100644 index 0000000..0c02ae0 --- /dev/null +++ b/wifi_main.py @@ -0,0 +1,109 @@ +import argparse +import time +from wifi_analyzer import ( + get_wifi_interface, + scan_wifi_networks, + get_channel_usage, + display_channel_usage_text, + monitor_current_connection_signal, + # Individual fetchers can also be used if needed, but monitor is more user-facing + get_current_connection_info_nmcli, + get_current_signal_strength_iw +) + +def main(): + parser = argparse.ArgumentParser(description="Wi-Fi Analyzer Tool for Linux.") + parser.add_argument( + "--interface", + help="Specify the Wi-Fi interface (e.g., wlan0). Auto-detected if not provided." + ) + parser.add_argument( + "--scan", + action="store_true", + help="Scan for available Wi-Fi networks and display their details." + ) + parser.add_argument( + "--channels", + action="store_true", + help="Display Wi-Fi channel usage analysis (requires a scan)." + ) + parser.add_argument( + "--monitor", + action="store_true", + help="Monitor the signal strength of the current Wi-Fi connection." + ) + parser.add_argument( + "--duration", + type=int, + default=10, + help="Duration for monitoring in seconds (default: 10s)." + ) + parser.add_argument( + "--interval", + type=int, + default=2, + help="Interval for signal strength updates in seconds (default: 2s)." + ) + parser.add_argument( + "--use_iw", + action="store_true", + help="Use 'iw' command for signal monitoring (provides dBm, may need sudo or specific permissions)." + ) + + args = parser.parse_args() + + if not args.scan and not args.channels and not args.monitor: + parser.print_help() + print("\nError: No action specified. Please use --scan, --channels, or --monitor.") + return + + target_interface = args.interface + if not target_interface: + print("Auto-detecting Wi-Fi interface...") + target_interface = get_wifi_interface() + if not target_interface: + print("Could not auto-detect Wi-Fi interface. Please specify one using --interface.") + return + print(f"Using interface: {target_interface}") + + networks_data = None # To store scan results if needed by multiple actions + + if args.scan or args.channels: # Scan is needed for both options + print(f"\nScanning Wi-Fi networks on interface {target_interface}...") + networks_data = scan_wifi_networks(interface=target_interface) + if not networks_data: + print("No networks found or an error occurred during scan.") + # If --channels was the only flag, we can't proceed. + # If --scan was also there, it will just show no networks. + + if args.scan: + if networks_data: + print(f"\n--- Found {len(networks_data)} Wi-Fi Networks ---") + for net in networks_data: + print(f" SSID: {net['ssid']}") + print(f" Signal: {net['signal']}%") + print(f" Channel: {net['channel']} (Band: {net['band']}, Freq: {net['frequency']})") + print(f" Security: {net['security']}") + print("-" * 20) + else: + print("No Wi-Fi networks detected or scan failed.") + + if args.channels: + if networks_data is None: # Should have been populated if --scan wasn't also specified + print("\nChannel analysis requires a network scan. Run with --scan or ensure scan succeeds.") + elif not networks_data: # Scan ran but found nothing + print("\nNo network data available to analyze for channel usage.") + else: + usage_2_4, usage_5 = get_channel_usage(networks_data) + display_channel_usage_text(usage_2_4, usage_5) + + if args.monitor: + monitor_current_connection_signal( + interface=target_interface, + duration=args.duration, + interval=args.interval, + use_iw=args.use_iw + ) + +if __name__ == "__main__": + main()