aiwaf 0.1.9.2.0__py3-none-any.whl → 0.1.9.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aiwaf might be problematic. Click here for more details.

aiwaf/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  default_app_config = "aiwaf.apps.AiwafConfig"
2
2
 
3
- __version__ = "0.1.9.2.0"
3
+ __version__ = "0.1.9.2.2"
4
4
 
5
5
  # Note: Middleware classes are available from aiwaf.middleware
6
6
  # Import them only when needed to avoid circular imports during Django app loading
@@ -0,0 +1,81 @@
1
+ from django.core.management.base import BaseCommand
2
+ from django.utils import timezone
3
+ from aiwaf.storage import get_blacklist_store, get_exemption_store, get_keyword_store
4
+ from datetime import timedelta
5
+ import json
6
+
7
+ def _sort(items, order):
8
+ reverse = (order == "newest")
9
+ return sorted(items, key=lambda x: x.get("created_at") or timezone.make_aware(timezone.datetime.min),
10
+ reverse=reverse)
11
+
12
+ def _filter_since(items, seconds):
13
+ if not seconds: return items
14
+ cutoff = timezone.now() - timedelta(seconds=seconds)
15
+ return [it for it in items if it.get("created_at") and it["created_at"] >= cutoff]
16
+
17
+ def _print_table(rows, headers):
18
+ widths = [len(h) for h in headers]
19
+ for r in rows:
20
+ for i, cell in enumerate(r):
21
+ widths[i] = max(widths[i], len(str(cell)))
22
+ print(" | ".join(h.ljust(widths[i]) for i, h in enumerate(headers)))
23
+ print("-+-".join("-" * w for w in widths))
24
+ for r in rows:
25
+ print(" | ".join(str(cell).ljust(widths[i]) for i, cell in enumerate(r)))
26
+
27
+ class Command(BaseCommand):
28
+ help = "Lister les données AIWAF (IPs bloquées, exemptions, mots-clés dynamiques)."
29
+
30
+ def add_arguments(self, parser):
31
+ grp = parser.add_mutually_exclusive_group()
32
+ grp.add_argument("--ips", action="store_true", help="Lister les IPs bloquées (défaut).")
33
+ grp.add_argument("--exemptions", action="store_true", help="Lister les IPs exemptées.")
34
+ grp.add_argument("--keywords", action="store_true", help="Lister les mots-clés dynamiques.")
35
+ grp.add_argument("--all", action="store_true", help="Tout lister.")
36
+ parser.add_argument("--format", choices=["table", "json"], default="table")
37
+ parser.add_argument("--limit", type=int, default=100)
38
+ parser.add_argument("--order", choices=["newest", "oldest"], default="newest")
39
+ parser.add_argument("--since", type=int, help="Fenêtre en secondes (ex: 86400 = 24h).")
40
+
41
+ def handle(self, *args, **o):
42
+ if not any([o["exemptions"], o["keywords"], o["all"]]): # défaut = ips
43
+ o["ips"] = True
44
+ payload = {}
45
+
46
+ if o["all"] or o["ips"]:
47
+ data = get_blacklist_store().get_all()
48
+ data = _filter_since(data, o.get("since"))
49
+ data = _sort(data, o["order"])[:o["limit"]]
50
+ payload["ips"] = data
51
+
52
+ if o["all"] or o["exemptions"]:
53
+ data = get_exemption_store().get_all()
54
+ data = _filter_since(data, o.get("since"))
55
+ data = _sort(data, o["order"])[:o["limit"]]
56
+ payload["exemptions"] = data
57
+
58
+ if o["all"] or o["keywords"]:
59
+ kws = get_keyword_store().get_top_keywords(o["limit"])
60
+ payload["keywords"] = [{"keyword": k} for k in kws]
61
+
62
+ if o["format"] == "json":
63
+ def _default(v):
64
+ try: return v.isoformat()
65
+ except Exception: return str(v)
66
+ self.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2, default=_default))
67
+ else:
68
+ if "ips" in payload:
69
+ print("\n== IPs bloquées ==")
70
+ rows = [[r.get("ip_address",""), r.get("reason",""), r.get("created_at","")]
71
+ for r in payload["ips"]]
72
+ _print_table(rows, ["ip_address", "reason", "created_at"])
73
+ if "exemptions" in payload:
74
+ print("\n== Exemptions ==")
75
+ rows = [[r.get("ip_address",""), r.get("reason",""), r.get("created_at","")]
76
+ for r in payload["exemptions"]]
77
+ _print_table(rows, ["ip_address", "reason", "created_at"])
78
+ if "keywords" in payload:
79
+ print("\n== Mots-clés dynamiques ==")
80
+ rows = [[r["keyword"]] for r in payload["keywords"]]
81
+ _print_table(rows, ["keyword"])
aiwaf/middleware.py CHANGED
@@ -103,47 +103,131 @@ class IPAndKeywordBlockMiddleware:
103
103
  return exempt_tokens
104
104
 
105
105
  def _get_legitimate_path_keywords(self):
106
- """Get keywords that are legitimate in URL paths"""
107
- # Extract from Django URL patterns
108
- legitimate_keywords = set()
106
+ """Get keywords that are legitimate in URL paths - uses same logic as trainer"""
107
+ # Import the enhanced function from trainer to ensure consistency
108
+ try:
109
+ from .trainer import get_legitimate_keywords
110
+ return get_legitimate_keywords()
111
+ except ImportError:
112
+ # Fallback to local implementation if trainer import fails
113
+ return self._get_legitimate_keywords_fallback()
114
+
115
+ def _get_legitimate_keywords_fallback(self):
116
+ """Fallback implementation matching trainer.py logic"""
117
+ legitimate = set()
109
118
 
110
- # Add common legitimate path segments
119
+ # Common legitimate path segments - matches trainer.py
111
120
  default_legitimate = {
112
- "profile", "user", "account", "settings", "dashboard",
113
- "home", "about", "contact", "help", "search", "list",
114
- "view", "edit", "create", "update", "delete", "detail",
115
- "api", "auth", "login", "logout", "register", "signup",
116
- "reset", "confirm", "activate", "verify", "page",
117
- "category", "tag", "post", "article", "blog", "news"
121
+ "profile", "user", "users", "account", "accounts", "settings", "dashboard",
122
+ "home", "about", "contact", "help", "search", "list", "lists",
123
+ "view", "views", "edit", "create", "update", "delete", "detail", "details",
124
+ "api", "auth", "login", "logout", "register", "signup", "signin",
125
+ "reset", "confirm", "activate", "verify", "page", "pages",
126
+ "category", "categories", "tag", "tags", "post", "posts",
127
+ "article", "articles", "blog", "blogs", "news", "item", "items",
128
+ "admin", "administration", "manage", "manager", "control", "panel",
129
+ "config", "configuration", "option", "options", "preference", "preferences"
118
130
  }
119
- legitimate_keywords.update(default_legitimate)
131
+ legitimate.update(default_legitimate)
132
+
133
+ # Extract keywords from Django URL patterns and app names - matches trainer.py
134
+ legitimate.update(self._extract_django_route_keywords())
120
135
 
121
136
  # Add from Django settings
122
137
  allowed_path_keywords = getattr(settings, "AIWAF_ALLOWED_PATH_KEYWORDS", [])
123
- legitimate_keywords.update(allowed_path_keywords)
138
+ legitimate.update(allowed_path_keywords)
124
139
 
125
- # Extract from actual Django URL patterns
126
- resolver = get_resolver()
127
- self._extract_path_keywords_from_urls(resolver.url_patterns, legitimate_keywords)
140
+ # Add exempt keywords
141
+ exempt_keywords = getattr(settings, "AIWAF_EXEMPT_KEYWORDS", [])
142
+ legitimate.update(exempt_keywords)
128
143
 
129
- return legitimate_keywords
144
+ return legitimate
130
145
 
131
- def _extract_path_keywords_from_urls(self, url_patterns, keywords, prefix=""):
132
- """Extract legitimate keywords from Django URL patterns"""
133
- for pattern in url_patterns:
134
- if hasattr(pattern, 'url_patterns'): # include()
135
- new_prefix = prefix + str(pattern.pattern).strip('^$/')
136
- self._extract_path_keywords_from_urls(pattern.url_patterns, keywords, new_prefix)
137
- else:
138
- # Extract static path segments from URL pattern
139
- pattern_str = str(pattern.pattern).strip('^$/')
140
- full_path = (prefix + '/' + pattern_str).strip('/')
146
+ def _extract_django_route_keywords(self):
147
+ """Extract legitimate keywords from Django URL patterns, app names, and model names - matches trainer.py"""
148
+ keywords = set()
149
+
150
+ try:
151
+ from django.urls.resolvers import URLResolver, URLPattern
152
+
153
+ # Extract from app names and labels
154
+ for app_config in apps.get_app_configs():
155
+ # Add app name and label
156
+ if app_config.name:
157
+ for segment in re.split(r'[._-]', app_config.name.lower()):
158
+ if len(segment) > 2:
159
+ keywords.add(segment)
160
+
161
+ if app_config.label and app_config.label != app_config.name:
162
+ for segment in re.split(r'[._-]', app_config.label.lower()):
163
+ if len(segment) > 2:
164
+ keywords.add(segment)
165
+
166
+ # Extract from model names in the app
167
+ try:
168
+ for model in app_config.get_models():
169
+ model_name = model._meta.model_name.lower()
170
+ if len(model_name) > 2:
171
+ keywords.add(model_name)
172
+ # Add plural form
173
+ if not model_name.endswith('s'):
174
+ keywords.add(f"{model_name}s")
175
+ except Exception:
176
+ continue
177
+
178
+ # Extract from URL patterns
179
+ def extract_from_pattern(pattern, prefix=""):
180
+ try:
181
+ if isinstance(pattern, URLResolver):
182
+ # Handle include() patterns
183
+ namespace = getattr(pattern, 'namespace', None)
184
+ if namespace:
185
+ for segment in re.split(r'[._-]', namespace.lower()):
186
+ if len(segment) > 2:
187
+ keywords.add(segment)
188
+
189
+ # Extract from the pattern itself
190
+ pattern_str = str(pattern.pattern)
191
+ for segment in re.findall(r'([a-zA-Z]\w{2,})', pattern_str):
192
+ keywords.add(segment.lower())
193
+
194
+ # Recurse into nested patterns
195
+ for nested_pattern in pattern.url_patterns:
196
+ extract_from_pattern(nested_pattern, prefix)
197
+
198
+ elif isinstance(pattern, URLPattern):
199
+ # Extract from URL pattern
200
+ pattern_str = str(pattern.pattern)
201
+ for segment in re.findall(r'([a-zA-Z]\w{2,})', pattern_str):
202
+ keywords.add(segment.lower())
203
+
204
+ # Extract from view name if available
205
+ if hasattr(pattern.callback, '__name__'):
206
+ view_name = pattern.callback.__name__.lower()
207
+ for segment in re.split(r'[._-]', view_name):
208
+ if len(segment) > 2 and segment != 'view':
209
+ keywords.add(segment)
210
+
211
+ except Exception:
212
+ pass
213
+
214
+ # Process all URL patterns
215
+ root_resolver = get_resolver()
216
+ for pattern in root_resolver.url_patterns:
217
+ extract_from_pattern(pattern)
141
218
 
142
- # Extract meaningful segments (not regex patterns)
143
- segments = re.findall(r'[a-zA-Z]{3,}', full_path)
144
- for seg in segments:
145
- if seg.lower() not in {'http', 'https', 'www'}:
146
- keywords.add(seg.lower())
219
+ except Exception as e:
220
+ # Silently continue if extraction fails
221
+ pass
222
+
223
+ # Filter out very common/generic words that might be suspicious
224
+ filtered_keywords = set()
225
+ for keyword in keywords:
226
+ if (len(keyword) >= 3 and
227
+ keyword not in ['www', 'com', 'org', 'net', 'int', 'str', 'obj', 'get', 'set', 'put', 'del']):
228
+ filtered_keywords.add(keyword)
229
+
230
+ return filtered_keywords
147
231
 
148
232
  def _is_malicious_context(self, request, segment):
149
233
  """Determine if a keyword appears in a malicious context"""
@@ -243,12 +327,29 @@ class IPAndKeywordBlockMiddleware:
243
327
 
244
328
  # Check segments against suspicious keywords
245
329
  for seg in segments:
330
+ is_suspicious = False
331
+ block_reason = ""
332
+
333
+ # Check if segment is in learned suspicious keywords
246
334
  if seg in suspicious_kw:
335
+ is_suspicious = True
336
+ block_reason = f"Learned keyword: {seg}"
337
+
338
+ # Also check if segment appears to be inherently malicious
339
+ elif (not path_exists and
340
+ seg not in self.legitimate_path_keywords and
341
+ (self._is_malicious_context(request, seg) or
342
+ any(malicious_pattern in seg for malicious_pattern in
343
+ ['hack', 'exploit', 'attack', 'malicious', 'evil', 'backdoor', 'inject', 'xss']))):
344
+ is_suspicious = True
345
+ block_reason = f"Inherently suspicious: {seg}"
346
+
347
+ if is_suspicious:
247
348
  # Additional context check before blocking
248
349
  if self._is_malicious_context(request, seg) or not path_exists:
249
350
  # Double-check exemption before blocking
250
351
  if not exemption_store.is_exempted(ip):
251
- BlacklistManager.block(ip, f"Keyword block: {seg} (context: malicious)")
352
+ BlacklistManager.block(ip, f"Keyword block: {block_reason}")
252
353
  # Check again after blocking attempt (exempted IPs won't be blocked)
253
354
  if BlacklistManager.is_blocked(ip):
254
355
  return JsonResponse({"error": "blocked"}, status=403)
aiwaf/trainer.py CHANGED
@@ -34,19 +34,34 @@ def path_exists_in_django(path: str) -> bool:
34
34
  from django.urls import get_resolver
35
35
  from django.urls.resolvers import URLResolver
36
36
 
37
- candidate = path.split("?")[0].lstrip("/")
37
+ candidate = path.split("?")[0].strip("/") # Remove query params and normalize slashes
38
+
39
+ # Try exact resolution first - this is the most reliable method
38
40
  try:
39
41
  get_resolver().resolve(f"/{candidate}")
40
42
  return True
41
43
  except:
42
44
  pass
43
-
44
- root = get_resolver()
45
- for p in root.url_patterns:
46
- if isinstance(p, URLResolver):
47
- prefix = p.pattern.describe().strip("^/")
48
- if prefix and candidate.startswith(prefix):
49
- return True
45
+
46
+ # Also try with trailing slash if it doesn't have one
47
+ if not candidate.endswith("/"):
48
+ try:
49
+ get_resolver().resolve(f"/{candidate}/")
50
+ return True
51
+ except:
52
+ pass
53
+
54
+ # Try without trailing slash if it has one
55
+ if candidate.endswith("/"):
56
+ try:
57
+ get_resolver().resolve(f"/{candidate.rstrip('/')}")
58
+ return True
59
+ except:
60
+ pass
61
+
62
+ # If direct resolution fails, be conservative
63
+ # Only do basic prefix matching for known include patterns
64
+ # but don't assume sub-paths exist just because the prefix exists
50
65
  return False
51
66
 
52
67
 
@@ -95,6 +110,9 @@ def get_legitimate_keywords() -> set:
95
110
  }
96
111
  legitimate.update(default_legitimate)
97
112
 
113
+ # Extract keywords from Django URL patterns and app names
114
+ legitimate.update(_extract_django_route_keywords())
115
+
98
116
  # Add from Django settings
99
117
  allowed_path_keywords = getattr(settings, "AIWAF_ALLOWED_PATH_KEYWORDS", [])
100
118
  legitimate.update(allowed_path_keywords)
@@ -106,6 +124,97 @@ def get_legitimate_keywords() -> set:
106
124
  return legitimate
107
125
 
108
126
 
127
+ def _extract_django_route_keywords() -> set:
128
+ """Extract legitimate keywords from Django URL patterns, app names, and model names"""
129
+ keywords = set()
130
+
131
+ try:
132
+ from django.urls import get_resolver
133
+ from django.urls.resolvers import URLResolver, URLPattern
134
+ from django.apps import apps
135
+
136
+ # Extract from app names and labels
137
+ for app_config in apps.get_app_configs():
138
+ # Add app name and label
139
+ if app_config.name:
140
+ for segment in re.split(r'[._-]', app_config.name.lower()):
141
+ if len(segment) > 2:
142
+ keywords.add(segment)
143
+
144
+ if app_config.label and app_config.label != app_config.name:
145
+ for segment in re.split(r'[._-]', app_config.label.lower()):
146
+ if len(segment) > 2:
147
+ keywords.add(segment)
148
+
149
+ # Extract from model names in the app
150
+ try:
151
+ for model in app_config.get_models():
152
+ model_name = model._meta.model_name.lower()
153
+ if len(model_name) > 2:
154
+ keywords.add(model_name)
155
+ # Add plural form
156
+ if not model_name.endswith('s'):
157
+ keywords.add(f"{model_name}s")
158
+ except Exception:
159
+ continue
160
+
161
+ # Extract from URL patterns
162
+ def extract_from_pattern(pattern, prefix=""):
163
+ try:
164
+ if isinstance(pattern, URLResolver):
165
+ # Handle include() patterns
166
+ namespace = getattr(pattern, 'namespace', None)
167
+ if namespace:
168
+ for segment in re.split(r'[._-]', namespace.lower()):
169
+ if len(segment) > 2:
170
+ keywords.add(segment)
171
+
172
+ # Extract from the pattern itself
173
+ pattern_str = str(pattern.pattern)
174
+ for segment in re.findall(r'([a-zA-Z]\w{2,})', pattern_str):
175
+ keywords.add(segment.lower())
176
+
177
+ # Recurse into nested patterns
178
+ for nested_pattern in pattern.url_patterns:
179
+ extract_from_pattern(nested_pattern, prefix)
180
+
181
+ elif isinstance(pattern, URLPattern):
182
+ # Extract from URL pattern
183
+ pattern_str = str(pattern.pattern)
184
+ for segment in re.findall(r'([a-zA-Z]\w{2,})', pattern_str):
185
+ keywords.add(segment.lower())
186
+
187
+ # Extract from view name if available
188
+ if hasattr(pattern.callback, '__name__'):
189
+ view_name = pattern.callback.__name__.lower()
190
+ for segment in re.split(r'[._-]', view_name):
191
+ if len(segment) > 2 and segment != 'view':
192
+ keywords.add(segment)
193
+
194
+ except Exception:
195
+ pass
196
+
197
+ # Process all URL patterns
198
+ root_resolver = get_resolver()
199
+ for pattern in root_resolver.url_patterns:
200
+ extract_from_pattern(pattern)
201
+
202
+ except Exception as e:
203
+ print(f"Warning: Could not extract Django route keywords: {e}")
204
+
205
+ # Filter out very common/generic words that might be suspicious
206
+ filtered_keywords = set()
207
+ for keyword in keywords:
208
+ if (len(keyword) >= 3 and
209
+ keyword not in ['www', 'com', 'org', 'net', 'int', 'str', 'obj', 'get', 'set', 'put', 'del']):
210
+ filtered_keywords.add(keyword)
211
+
212
+ if filtered_keywords:
213
+ print(f"🔗 Extracted {len(filtered_keywords)} legitimate keywords from Django routes and apps")
214
+
215
+ return filtered_keywords
216
+
217
+
109
218
  def _read_all_logs() -> list[str]:
110
219
  lines = []
111
220
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiwaf
3
- Version: 0.1.9.2.0
3
+ Version: 0.1.9.2.2
4
4
  Summary: AI-powered Web Application Firewall
5
5
  Home-page: https://github.com/aayushgauba/aiwaf
6
6
  Author: Aayush Gauba
@@ -94,14 +94,19 @@ aiwaf/
94
94
  - Burst count
95
95
  - Total 404s
96
96
 
97
- - **Enhanced Dynamic Keyword Learning**
97
+ - **Enhanced Dynamic Keyword Learning with Django Route Protection**
98
98
  - **Smart Context-Aware Learning**: Only learns keywords from suspicious requests on non-existent paths
99
- - **Legitimate Path Protection**: Automatically excludes keywords from valid Django URLs (like `/profile/`, `/admin/`)
99
+ - **Automatic Django Route Extraction**: Automatically excludes keywords from:
100
+ - Valid Django URL patterns (`/profile/`, `/admin/`, `/api/`, etc.)
101
+ - Django app names and model names (users, posts, categories)
102
+ - View function names and URL namespaces
103
+ - **Unified Logic**: Both trainer and middleware use identical legitimate keyword detection
100
104
  - **Configuration Options**:
101
105
  - `AIWAF_ALLOWED_PATH_KEYWORDS` - Explicitly allow certain keywords in legitimate paths
102
106
  - `AIWAF_EXEMPT_KEYWORDS` - Keywords that should never trigger blocking
103
107
  - **Automatic Cleanup**: Keywords from `AIWAF_EXEMPT_PATHS` are automatically removed from the database
104
108
  - **False Positive Prevention**: Stops learning legitimate site functionality as "malicious"
109
+ - **Inherent Malicious Detection**: Middleware also blocks obviously malicious keywords (`hack`, `exploit`, `attack`) even if not yet learned
105
110
 
106
111
  - **File‑Extension Probing Detection**
107
112
  Tracks repeated 404s on common extensions (e.g. `.php`, `.asp`) and blocks IPs.
@@ -1,18 +1,19 @@
1
- aiwaf/__init__.py,sha256=Bn2DcnLiYvx-vkOUfIbQtnKUDidKK1rAUNuXuPa36MM,220
1
+ aiwaf/__init__.py,sha256=EwzM3mRDf7i5IVX0-pTMpVFRf51lpIIbh6ZzkzOw10M,220
2
2
  aiwaf/apps.py,sha256=nCez-Ptlv2kaEk5HenA8b1pATz1VfhrHP1344gwcY1A,142
3
3
  aiwaf/blacklist_manager.py,sha256=LYCeKFB-7e_C6Bg2WeFJWFIIQlrfRMPuGp30ivrnhQY,1196
4
4
  aiwaf/decorators.py,sha256=IUKOdM_gdroffImRZep1g1wT6gNqD10zGwcp28hsJCs,825
5
- aiwaf/middleware.py,sha256=D1HavBGJbpPneOtkkCVFddlOQwCdoWcugmHOvn5THDU,22614
5
+ aiwaf/middleware.py,sha256=8EC4AKfUjHhmVSKpquimkMUebBekr92pqyVF97wlbx0,27408
6
6
  aiwaf/middleware_logger.py,sha256=LWZVDAnjh6CGESirA8eMbhGgJKB7lVDGRQqVroH95Lo,4742
7
7
  aiwaf/models.py,sha256=vQxgY19BDVMjoO903UNrTZC1pNoLltMU6wbyWPoAEns,2719
8
8
  aiwaf/storage.py,sha256=5ImrZMRn3u7HNsPH0fDjWhDrD2tgG2IHVnOXtLz0fk4,10253
9
- aiwaf/trainer.py,sha256=UHkfrbJI47bGJPCz0Vws6r23WvGpemMHf5ScHWG_I1I,14568
9
+ aiwaf/trainer.py,sha256=U-X79nFhSTEbVexFHo3IXFf1HgvXrFnQ__WqTar0o4M,19118
10
10
  aiwaf/utils.py,sha256=BJk5vJCYdGPl_4QQiknjhCbkzv5HZCXgFcBJDMJpHok,3390
11
11
  aiwaf/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  aiwaf/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  aiwaf/management/commands/add_exemption.py,sha256=U_ByfJw1EstAZ8DaSoRb97IGwYzXs0DBJkVAqeN4Wak,1128
14
14
  aiwaf/management/commands/add_ipexemption.py,sha256=sSf3d9hGK9RqqlBYkCrnrd8KZWGT-derSpoWnEY4H60,952
15
15
  aiwaf/management/commands/aiwaf_diagnose.py,sha256=nXFRhq66N4QC3e4scYJ2sUngJce-0yDxtBO3R2BllRM,6134
16
+ aiwaf/management/commands/aiwaf_list.py,sha256=tZK3FugApmPxxvmoB4-nLY9fpZJgiRtD137Bre5hEp8,3839
16
17
  aiwaf/management/commands/aiwaf_logging.py,sha256=FCIqULn2tii2vD9VxL7vk3PV4k4vr7kaA00KyaCExYY,7692
17
18
  aiwaf/management/commands/aiwaf_reset.py,sha256=pcF0zOYDSqjpCwDtk2HYJZLgr76td8OFRENtl20c1dQ,7472
18
19
  aiwaf/management/commands/check_dependencies.py,sha256=GOZl00pDwW2cJjDvIaCeB3yWxmeYcJDRTIpmOTLvy2c,37204
@@ -28,8 +29,8 @@ aiwaf/management/commands/test_exemption_fix.py,sha256=ngyGaHUCmQQ6y--6j4q1viZJt
28
29
  aiwaf/resources/model.pkl,sha256=5t6h9BX8yoh2xct85MXOO60jdlWyg1APskUOW0jZE1Y,1288265
29
30
  aiwaf/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
31
  aiwaf/templatetags/aiwaf_tags.py,sha256=XXfb7Tl4DjU3Sc40GbqdaqOEtKTUKELBEk58u83wBNw,357
31
- aiwaf-0.1.9.2.0.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
32
- aiwaf-0.1.9.2.0.dist-info/METADATA,sha256=HM_8Dh89XWhMKtLDkqrvA7fxzR97F9Ph1sY2bYOk9Mc,26414
33
- aiwaf-0.1.9.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
- aiwaf-0.1.9.2.0.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
35
- aiwaf-0.1.9.2.0.dist-info/RECORD,,
32
+ aiwaf-0.1.9.2.2.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
33
+ aiwaf-0.1.9.2.2.dist-info/METADATA,sha256=NFu9QZWsGPcmAJaHeJroSXZL_PstDr33NbSffV94bLQ,26824
34
+ aiwaf-0.1.9.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ aiwaf-0.1.9.2.2.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
36
+ aiwaf-0.1.9.2.2.dist-info/RECORD,,