├── .github └── workflows │ ├── codeql-analysis.yml │ └── python-tests.yml ├── .gitignore ├── .pylintrc ├── .style.yapf ├── LICENSE.txt ├── README.rst ├── p0f ├── __init__.py ├── django │ ├── __init__.py │ └── middleware.py └── examples │ └── django_models.py └── setup.py /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '29 14 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/python-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run python tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.6, 3.7, 3.8] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install pycodestyle isort pylint yapf django 23 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 24 | - name: Check pycodestyle 25 | run: | 26 | pycodestyle --ignore E501,E402 --exclude=.git,dev3 p0f 27 | - name: Run pylint 28 | run: | 29 | pylint p0f 30 | - name: Check formatting 31 | run: | 32 | isort --recursive p0f; yapf --recursive -i . 33 | git diff --exit-code # This fails if isort&yapf combo made any changes 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | syntax:glob 2 | *.egg-info 3 | build 4 | dist 5 | *.pyc 6 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [format] 2 | max-line-length=125 3 | 4 | [messages control] 5 | disable=line-too-long,missing-docstring,no-self-use,fixme,bad-indentation,bad-continuation,invalid-name,too-many-locals,duplicate-code,too-many-branches,wrong-import-order 6 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = pep8 3 | allow_split_before_dict_value = false 4 | blank_line_before_nested_class_or_def = false 5 | coalesce_brackets = true 6 | column_limit = 125 7 | dedent_closing_brackets = true 8 | each_dict_entry_on_separate_line = true 9 | join_multiple_lines = true 10 | spaces_around_power_operator = true 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021, Olli Jarva 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | p0f Python API client 2 | ===================== 3 | 4 | This is a simple API client for p0f3, available at 5 | http://lcamtuf.coredump.cx/p0f3/ . It is not compatible with version 2.x 6 | or 1.x. Start p0f with ``-s path/to/unix_socket`` option. 7 | 8 | Basic usage: 9 | 10 | :: 11 | 12 | from p0f import P0f, P0fException 13 | 14 | data = None 15 | p0f = P0f("p0f.sock") # point this to socket defined with "-s" argument. 16 | try: 17 | data = p0f.get_info("192.168.0.1") 18 | except P0fException, e: 19 | # Invalid query was sent to p0f. Maybe the API has changed? 20 | print e 21 | except KeyError, e: 22 | # No data is available for this IP address. 23 | print e 24 | except ValueError, e: 25 | # p0f returned invalid constant values. Maybe the API has changed? 26 | print e 27 | 28 | if data: 29 | print "First seen:", data["first_seen"] 30 | print "Last seen:", data["last_seen"] 31 | 32 | 33 | Django integration 34 | ------------------ 35 | 36 | See examples/django_models.py for complete Django model of the data returned by p0f. 37 | 38 | Django middleware is available in ``p0f.django.middleware``. 39 | 40 | To use, add ``P0FSOCKET = "path/to/p0f_unix_socket"`` to your project's settings.py, 41 | and ``p0f.django.middleware.P0fMiddleware`` to ``MIDDLEWARE_CLASSES``. 42 | 43 | The middleware adds ``p0f`` attribute to all incoming requests. ``request.p0f`` is 44 | None if connection to p0f failed or p0f did not return data for remote IP address. 45 | 46 | Data fields 47 | ----------- 48 | 49 | Parts of these descriptions are shamelessly copied from 50 | http://lcamtuf.coredump.cx/p0f3/README : 51 | 52 | By default, following fields are parsed: 53 | 54 | - datetime: **first_seen** 55 | - datetime: **last_seen** 56 | - timedelta: **uptime** 57 | - int: **uptime_sec** 58 | - timedelta: **up_mod_days** 59 | - datetime: **last_nat** 60 | - datetime: **last_chg** 61 | 62 | Additionally, **bad_sw** and **os_match_q** are validated. "ValueError" 63 | is raised, if incorrect value is encountered. For all empty fields, 64 | None is used instead of empty strings or constants: 65 | 66 | - **uptime_min** 67 | - **uptime_sec** 68 | - **uptime** 69 | - **up_mod_days** 70 | - **last_nat** 71 | - **last_chg** 72 | - **distance** 73 | - **bad_sw** 74 | - **os_name** 75 | - **os_flavor** 76 | - **http_flavor** 77 | - **link_type** 78 | - **language** 79 | 80 | This parsing and validation can be disabled with 81 | 82 | :: 83 | 84 | p0f.get_info("192.168.0.1", True) 85 | 86 | Full descriptions of the fields: 87 | 88 | - int: **first_seen** - unix time (seconds) of first observation of the host. 89 | - int: **last_seen** - unix time (seconds) of most recent traffic. 90 | - int: **total_conn** - total number of connections seen. 91 | - int: **uptime_min** - calculated system uptime, in minutes. Zero if not known. 92 | - int: **up_mod_days** - uptime wrap-around interval, in days. 93 | - int: **last_nat** - time of the most recent detection of IP sharing (NAT, load balancing, proxying). Zero if never detected. 94 | - int: **last_chg** - time of the most recent individual OS mismatch (e.g., due to multiboot or IP reuse). 95 | - int: **distance** - system distance (derived from TTL; -1 if no data). 96 | - int: **bad_sw** - p0f thinks the User-Agent or Server strings aren't accurate. The value of 1 means OS difference (possibly due to proxying), while 2 means an outright mismatch. NOTE: If User-Agent is not present at all, this value stays at 0. 97 | - int: **os_match_q** - OS match quality: 0 for a normal match; 1 for fuzzy (e.g., TTL or DF difference); 2 for a generic signature; and 3 for both. 98 | - string: **os_name** - Name of the most recent positively matched OS. If OS not known, os_name is empty string. NOTE: If the host is first seen using an known system and then switches to an unknown one, this field is not reset. 99 | - string: **os_flavor** - OS version. May be empty if no data. 100 | - string: **http_name** - most recent positively identified HTTP application (e.g. 'Firefox'). 101 | - string: **http_flavor** - version of the HTTP application, if any. 102 | - string: **link_type** - network link type, if recognized. 103 | - string: **language** - system language, if recognized. 104 | 105 | License 106 | ------- 107 | 108 | See LICENSE.txt -------------------------------------------------------------------------------- /p0f/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014 Olli Jarva 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import datetime 26 | import socket 27 | import struct 28 | 29 | 30 | class P0f: 31 | """ This class is used to query data from p0f3, available from 32 | http://lcamtuf.coredump.cx/p0f3/ 33 | 34 | This is not compatible with version 1.x or 2.x of p0f. 35 | 36 | Start p0f3 with "-s name" option (e.g "-s p0f.sock"). Do note 37 | p0f3 restricts the number of simultaneous API connections. 38 | You have to either delete instance of this class or run 39 | .close() to disconnect. New connection is automatically 40 | opened when running get_info() 41 | 42 | 43 | p0f = P0f("p0f.sock") 44 | data = p0f.get_info("192.168.2.1") 45 | p0f.close() 46 | """ 47 | RESPONSE_FIELDS = [ 48 | "magic", "status", "first_seen", "last_seen", "total_conn", "uptime_min", "up_mod_days", "last_nat", "last_chg", 49 | "distance", "bad_sw", "os_match_q", "os_name", "os_flavor", "http_name", "http_flavor", "link_type", "language" 50 | ] 51 | RESPONSE_FMT = "IbIIIIIIIhbb32s32s32s32s32s32s" 52 | 53 | RESPONSE_NO_MATCH = 32 54 | RESPONSE_OK = 16 55 | RESPONSE_BAD_QUERY = 0 56 | 57 | RESPONSE_STATUS = {32: RESPONSE_NO_MATCH, 16: RESPONSE_OK, 0: RESPONSE_BAD_QUERY} 58 | 59 | RESPONSE_DATETIME_PARSE = ["first_seen", "last_seen", "last_nat", "last_chg"] 60 | 61 | OS_MATCH_NORMAL = 0 62 | OS_MATCH_FUZZY = 1 63 | OS_MATCH_GENERIC = 2 64 | OS_MATCH_BOTH = 3 65 | 66 | RESPONSE_OS_MATCH = {0: OS_MATCH_NORMAL, 1: OS_MATCH_FUZZY, 2: OS_MATCH_GENERIC, 3: OS_MATCH_BOTH} 67 | 68 | BAD_SW_NA = 0 69 | BAD_SW_OS_MISMATCH = 1 70 | BAD_SW_MISMATCH = 2 71 | 72 | RESPONSE_BAD_SW = {0: BAD_SW_NA, 1: BAD_SW_OS_MISMATCH, 2: BAD_SW_MISMATCH} 73 | 74 | def __init__(self, socket_path, **kwargs): 75 | self.socket_path = socket_path 76 | self._client = None 77 | self.timeout = kwargs.get("timeout", 0.1) 78 | 79 | @property 80 | def client(self): 81 | """ Returns (cached) socket connection to p0f """ 82 | if not self._client: 83 | self._client = socket.socket(socket.AF_UNIX) 84 | self._client.settimeout(self.timeout) 85 | self._client.connect(self.socket_path) 86 | return self._client 87 | 88 | def close(self): 89 | """ Closes (cached) socket connection """ 90 | if not self._client: 91 | return 92 | self._client.close() 93 | 94 | def get_info(self, ip_address, return_raw_data=False): 95 | """ Returns information retrieved from p0f. 96 | Raises 97 | P0fException for invalid queries 98 | KeyError if no data is available 99 | socket.error if socket is disconnected 100 | ValueError if invalid constant value is 101 | returned. 102 | 103 | Returns dictionary matching to fields defined in 104 | http://lcamtuf.coredump.cx/p0f3/README 105 | under "API access". 106 | """ 107 | 108 | address_class = socket.AF_INET 109 | if ":" in ip_address: 110 | address_class = socket.AF_INET6 111 | packed_address = socket.inet_pton(address_class, ip_address) 112 | data_send = struct.pack("Ib", 0x50304601, 4) + packed_address 113 | if address_class == socket.AF_INET: 114 | data_send += struct.pack("12x") 115 | self.client.send(data_send) 116 | data_received = self.client.recv(1024) 117 | values = struct.unpack(self.RESPONSE_FMT, data_received) 118 | 119 | data_in = {} 120 | for i, value in enumerate(values): 121 | value = values[i] 122 | if isinstance(value, str): 123 | value = value.replace("\x00", "") 124 | data_in[self.RESPONSE_FIELDS[i]] = value 125 | 126 | if data_in["magic"] != 0x50304602: 127 | raise P0fException("Server returned invalid magic number") 128 | 129 | status = self.RESPONSE_STATUS[data_in["status"]] 130 | if status == self.RESPONSE_BAD_QUERY: 131 | raise P0fException("Improperly formatted query sent to p0f") 132 | if status == self.RESPONSE_NO_MATCH: 133 | raise KeyError("No data available in p0f for %s" % ip_address) 134 | if return_raw_data: 135 | return data_in 136 | 137 | return P0f.format_data(data_in) 138 | 139 | @classmethod 140 | def format_data(cls, data_in): 141 | """ Parses p0f response to datetime, validates constants and 142 | replaces empty values with None """ 143 | for field in cls.RESPONSE_DATETIME_PARSE: 144 | if data_in[field] == 0: 145 | data_in[field] = None 146 | continue 147 | data_in[field] = datetime.datetime.fromtimestamp(data_in[field]) 148 | data_in["up_mod_days"] = datetime.timedelta(days=data_in["up_mod_days"]) 149 | 150 | if data_in["uptime_min"] == 0: 151 | data_in["uptime"] = None 152 | data_in["uptime_min"] = None 153 | data_in["uptime_sec"] = None 154 | else: 155 | data_in["uptime_sec"] = data_in["uptime_min"] * 60 156 | data_in["uptime"] = datetime.timedelta(seconds=data_in["uptime_sec"]) 157 | 158 | if data_in["os_match_q"] not in cls.RESPONSE_OS_MATCH: 159 | raise ValueError("p0f provided invalid value for os_match_q: %s" % data_in["os_match_q"]) 160 | if data_in["bad_sw"] not in cls.RESPONSE_BAD_SW: 161 | raise ValueError("p0f responded with invalid bad_sw: %s" % data_in["bad_sw"]) 162 | 163 | if data_in["distance"] == -1: 164 | data_in["distance"] = None 165 | 166 | for field in ("up_mod_days", "last_nat", "last_chg", "bad_sw"): 167 | if data_in[field] == 0: 168 | data_in[field] = None 169 | 170 | for field in ("os_name", "os_flavor", "http_flavor", "link_type", "language"): 171 | if len(data_in[field]) == 0: 172 | data_in[field] = None 173 | 174 | return data_in 175 | 176 | 177 | class P0fException(Exception): 178 | """ Raised when server returns invalid data """ 179 | 180 | 181 | def main(): 182 | """ This is a testing method, executed when 183 | this file is executed instead of imported. """ 184 | p0f_client = P0f("p0f.sock") 185 | print(p0f_client.get_info("10.1.0.2")) 186 | 187 | 188 | if __name__ == '__main__': 189 | main() 190 | -------------------------------------------------------------------------------- /p0f/django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojarva/p0f3-api-py/6413c151c9714bbda4e5fc67fc34da2618e8c881/p0f/django/__init__.py -------------------------------------------------------------------------------- /p0f/django/middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | Adds p0f attribute to request object. 3 | 4 | To enable, add 5 | 6 | 'p0f.django.middleware.P0fMiddleware', 7 | 8 | to "MIDDLEWARE_CLASSES" in your project's settings.py. Also, add 9 | 10 | P0FSOCKET="/path/to/p0f_socket" 11 | 12 | to the same file. 13 | 14 | Start p0f with "-s /path/to/p0f_socket". 15 | """ 16 | 17 | import logging 18 | import socket 19 | 20 | from django.conf import settings 21 | from django.core.exceptions import ImproperlyConfigured, MiddlewareNotUsed 22 | 23 | import p0f 24 | 25 | log = logging.getLogger(__name__) 26 | 27 | 28 | class P0fMiddleware: # pylint: disable=too-few-public-methods 29 | """ Adds "p0f" attribute to request. Requires P0FSOCKET setting in Django settings.py """ 30 | def __init__(self): 31 | try: 32 | enabled = settings.P0FENABLED 33 | if not enabled: 34 | raise MiddlewareNotUsed 35 | except AttributeError: 36 | pass 37 | 38 | try: 39 | settings.P0FSOCKET 40 | except AttributeError as ex: 41 | log.error("P0FSOCKET is not configured.") 42 | raise ImproperlyConfigured( 43 | "P0FSOCKET is not configured. This middleware does not run without path to p0f unix socket" 44 | ) from ex 45 | 46 | def process_request(self, request): 47 | remote_info = None 48 | try: 49 | p0fapi = p0f.P0f(settings.P0FSOCKET) 50 | remote_info = p0fapi.get_info(request.META.get("REMOTE_ADDR")) 51 | except socket.error as e: 52 | log.error("p0f API call returned %r", e) 53 | except KeyError as e: 54 | log.warning("No data available for %s - is p0f listening the right interface?", request.META.get("REMOTE_ADDR")) 55 | except (ValueError, p0f.P0fException) as e: 56 | log.warning("internal error: %r", e) 57 | 58 | request.p0f = remote_info 59 | -------------------------------------------------------------------------------- /p0f/examples/django_models.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a complete Django ORM model for p0f data. 3 | 4 | The easiest way to add new data is something along the lines of 5 | 6 | 7 | del data_from_p0f["magic"] 8 | del data_from_p0f["uptime"] 9 | del data_from_p0f["uptime_min"] 10 | P0fRecord.objects.create(**data_from_p0f) 11 | 12 | """ 13 | 14 | from django.db import models 15 | 16 | 17 | class P0fRecord(models.Model): 18 | OS_NORMAL = 0 19 | OS_FUZZY = 1 20 | OS_GENERIC = 2 21 | OS_BOTH = 3 22 | 23 | OS_MATCH = ( 24 | (OS_NORMAL, "Normal"), 25 | (OS_FUZZY, "Fuzzy"), 26 | (OS_GENERIC, "Generic signature"), 27 | (OS_BOTH, "Using both"), 28 | ) 29 | 30 | BAD_SW_OS_MISMATCH = 1 31 | BAD_SW_MISMATCH = 2 32 | 33 | BAD_SW = ( 34 | (BAD_SW_OS_MISMATCH, "OS mismatch"), 35 | (BAD_SW_MISMATCH, "Mismatch"), 36 | ) 37 | 38 | class Meta: 39 | ordering = ["first_seen", "last_seen"] 40 | get_latest_by = "last_seen" 41 | 42 | def __unicode__(self): 43 | return u"%s: %s @ %s" % (self.remote_ip, self.os_name, self.first_seen) 44 | 45 | created_at = models.DateTimeField(auto_now_add=True) 46 | updated_at = models.DateTimeField(auto_now=True) 47 | 48 | remote_ip = models.GenericIPAddressField() 49 | 50 | first_seen = models.DateTimeField(help_text="First observation of the host") 51 | last_seen = models.DateTimeField(help_text="Most recent observation of the host") 52 | total_conn = models.IntegerField(help_text="Total number of connections seen") 53 | uptime_sec = models.IntegerField( 54 | null=True, blank=True, help_text="Calculated system uptime in seconds, None if not available" 55 | ) 56 | up_mod_days = models.IntegerField(null=True, blank=True, help_text="Uptime wrap-around time in days") 57 | last_nat = models.DateTimeField( 58 | null=True, blank=True, help_text="Time of the most recent detection of IP sharing, None if never detected" 59 | ) 60 | last_chg = models.DateTimeField( 61 | null=True, blank=True, help_text="Time of the most recent OS mismatch (e.g multiboot or IP reuse)" 62 | ) 63 | distance = models.IntegerField(null=True, blank=True, help_text="System distance (from TTL). None if no data") 64 | bad_sw = models.CharField( 65 | max_length=1, choices=BAD_SW, null=True, help_text="Whether user-agent and/or Server string are accurate" 66 | ) 67 | os_match_q = models.CharField(max_length=1, choices=OS_MATCH, help_text="OS match quality") 68 | 69 | os_name = models.CharField(max_length=32, null=True, blank=True, help_text="Name of the most recently matched OS") 70 | os_flavor = models.CharField(max_length=32, null=True, blank=True, help_text="OS version") 71 | http_name = models.CharField( 72 | max_length=32, null=True, blank=True, help_text="Name of the most recently identified HTTP application" 73 | ) 74 | http_flavor = models.CharField( 75 | max_length=32, null=True, blank=True, help_text="Flavor of the most recently identified HTTP application" 76 | ) 77 | link_type = models.CharField(max_length=32, null=True, blank=True, help_text="Network link type, if autodetected") 78 | language = models.CharField(max_length=32, null=True, blank=True, help_text="Remote system language, if recognized") 79 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | 5 | # Utility function to read the README file. 6 | # Used for the long_description. It's nice, because now 1) we have a top level 7 | # README file and 2) it's easier to type in the README file than to put a raw 8 | # string in below ... 9 | def read(fname): 10 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 11 | 12 | 13 | setup( 14 | name="p0f", 15 | version="1.0.0", 16 | author="Olli Jarva", 17 | author_email="olli@jarva.fi", 18 | description=("API client for p0f3"), 19 | license="MIT", 20 | keywords="p0f fingerprinting API client", 21 | url="https://github.com/ojarva/p0f3-api-py", 22 | packages=['p0f'], 23 | long_description=read('README.rst'), 24 | download_url="https://github.com/ojarva/p0f3-api-py", 25 | bugtracker_url="https://github.com/ojarva/p0f3-api-py/issues", 26 | classifiers=[ 27 | "Development Status :: 5 - Production/Stable", 28 | "Topic :: Software Development :: Libraries", 29 | "Topic :: System :: Networking :: Monitoring", 30 | "License :: OSI Approved :: MIT License", 31 | ], 32 | ) 33 | --------------------------------------------------------------------------------