feat: Add LeakPasswords config

pull/15353/head
wangruidong 2025-05-06 18:20:34 +08:00 committed by 老广
parent 0bdbb6fd84
commit 089a5f50f4
15 changed files with 161 additions and 12 deletions

View File

@ -12,6 +12,7 @@ from accounts.models import Account, AccountRisk, RiskChoice
from assets.automations.base.manager import BaseManager
from common.const import ConfirmOrIgnore
from common.decorators import bulk_create_decorator, bulk_update_decorator
from settings.models import LeakPasswords
@bulk_create_decorator(AccountRisk)
@ -157,10 +158,8 @@ class CheckLeakHandler(BaseCheckHandler):
if not account.secret:
return False
sql = 'SELECT 1 FROM passwords WHERE password = ? LIMIT 1'
self.cursor.execute(sql, (account.secret,))
leak = self.cursor.fetchone() is not None
return leak
is_exist = LeakPasswords.objects.using('sqlite').filter(password=account.secret).exists()
return is_exist
def clean(self):
self.cursor.close()

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.13 on 2025-05-06 10:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0006_alter_accountrisk_username_and_more'),
]
operations = [
migrations.AlterField(
model_name='account',
name='connectivity',
field=models.CharField(choices=[('-', 'Unknown'), ('na', 'N/A'), ('ok', 'OK'), ('err', 'Error'), ('auth_err', 'Authentication error'), ('sudo_err', 'Sudo permission error'), ('password_err', 'Invalid password error'), ('openssh_key_err', 'OpenSSH key error'), ('ntlm_err', 'NTLM credentials rejected error'), ('create_dir_err', 'Create directory error')], default='-', max_length=16, verbose_name='Connectivity'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.13 on 2025-05-06 10:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0018_rename_domain_zone'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='connectivity',
field=models.CharField(choices=[('-', 'Unknown'), ('na', 'N/A'), ('ok', 'OK'), ('err', 'Error'), ('auth_err', 'Authentication error'), ('sudo_err', 'Sudo permission error'), ('password_err', 'Invalid password error'), ('openssh_key_err', 'OpenSSH key error'), ('ntlm_err', 'NTLM credentials rejected error'), ('create_dir_err', 'Create directory error')], default='-', max_length=16, verbose_name='Connectivity'),
),
]

View File

@ -187,7 +187,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
'PermedAsset', 'PermedAccount', 'MenuPermission',
'Permission', 'TicketSession', 'ApplyLoginTicket',
'ApplyCommandTicket', 'ApplyLoginAssetTicket',
'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable'
'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable', 'LeakPasswords'
}
include_models = {'UserSession'}
for i, app in enumerate(apps.get_models(), 1):

View File

@ -323,7 +323,7 @@ class AuthPostCheckMixin:
def _check_passwd_is_too_simple(cls, user: User, password):
if not user.is_auth_backend_model():
return
if user.check_passwd_too_simple(password):
if user.check_passwd_too_simple(password) or user.check_leak_password(password):
message = _('Your password is too simple, please change it for security')
url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
raise errors.PasswordTooSimple(url)

View File

@ -1548,5 +1548,6 @@
"currentTime": "当前时间",
"assetId": "资产 ID",
"assetName": "资产名称",
"assetAddress": "资产地址"
"assetAddress": "资产地址",
"LeakPasswordList": "弱密码列表"
}

View File

@ -705,7 +705,7 @@ class Config(dict):
'FILE_UPLOAD_SIZE_LIMIT_MB': 200,
'TICKET_APPLY_ASSET_SCOPE': 'all',
'LEAK_PASSWORD_DB_PATH': os.path.join(PROJECT_DIR, 'data', 'leak_password.db'),
'LEAK_PASSWORD_DB_PATH': os.path.join(PROJECT_DIR, 'data', 'leak_passwords.db'),
# Ansible Receptor
'RECEPTOR_ENABLED': False,

View File

@ -4,9 +4,11 @@ from django.conf import settings
from django.core.cache import cache
from rest_framework.generics import ListAPIView, CreateAPIView
from rest_framework.views import Response
from rest_framework.viewsets import ModelViewSet
from users.utils import LoginIpBlockUtil
from ..serializers import SecurityBlockIPSerializer
from ..models import LeakPasswords
from ..serializers import SecurityBlockIPSerializer, LeakPasswordPSerializer
class BlockIPSecurityAPI(ListAPIView):
@ -56,3 +58,16 @@ class UnlockIPSecurityAPI(CreateAPIView):
for ip in ips:
LoginIpBlockUtil(ip).clean_block_if_need()
return Response(status=200)
class LeakPasswordViewSet(ModelViewSet):
serializer_class = LeakPasswordPSerializer
model = LeakPasswords
rbac_perms = {
'*': 'settings.change_security'
}
queryset = LeakPasswords.objects.none()
search_fields = ['password']
def get_queryset(self):
return LeakPasswords.objects.using('sqlite').all()

View File

@ -9,3 +9,9 @@ class SettingsConfig(AppConfig):
def ready(self):
from . import signal_handlers # noqa
from . import tasks # noqa
from .models import init_sqlite_db, register_sqlite_connection
try:
init_sqlite_db()
register_sqlite_connection()
except Exception:
pass

View File

@ -0,0 +1,24 @@
# Generated by Django 4.1.13 on 2025-05-06 10:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('settings', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='LeakPasswords',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('password', models.CharField(max_length=1024, verbose_name='Password')),
],
options={
'db_table': 'passwords',
'managed': False,
},
),
]

View File

@ -1,10 +1,12 @@
import json
import os
import shutil
from django.conf import settings
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.db import models
from django.db import models, connections
from django.db.utils import ProgrammingError, OperationalError
from django.utils.translation import gettext_lazy as _
from rest_framework.utils.encoders import JSONEncoder
@ -208,3 +210,38 @@ def get_chatai_data():
data['model'] = settings.DEEPSEEK_MODEL
return data
def init_sqlite_db():
db_path = settings.LEAK_PASSWORD_DB_PATH
if not os.path.isfile(db_path):
db_path = settings.LEAK_PASSWORD_DB_PATH
src = os.path.join(
settings.APPS_DIR, 'accounts', 'automations',
'check_account', 'leak_passwords.db'
)
shutil.copy(src, db_path)
logger.info(f'init sqlite db {db_path}')
return db_path
def register_sqlite_connection():
connections.databases['sqlite'] = {
'ENGINE': 'django.db.backends.sqlite3',
'ATOMIC_REQUESTS': False,
'NAME': settings.LEAK_PASSWORD_DB_PATH,
'TIME_ZONE': None,
'CONN_HEALTH_CHECKS': False,
'CONN_MAX_AGE': 0,
'OPTIONS': {},
'AUTOCOMMIT': True,
}
class LeakPasswords(models.Model):
id = models.AutoField(primary_key=True)
password = models.CharField(max_length=1024, verbose_name=_("Password"))
class Meta:
db_table = 'passwords'
managed = False

View File

@ -7,9 +7,11 @@ __all__ = [
'SecurityPasswordRuleSerializer', 'SecuritySessionSerializer',
'SecurityAuthSerializer', 'SecuritySettingSerializer',
'SecurityLoginLimitSerializer', 'SecurityBasicSerializer',
'SecurityBlockIPSerializer'
'SecurityBlockIPSerializer', 'LeakPasswordPSerializer'
]
from settings.models import LeakPasswords
class SecurityPasswordRuleSerializer(serializers.Serializer):
SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(
@ -269,3 +271,20 @@ class SecuritySettingSerializer(
class SecurityBlockIPSerializer(serializers.Serializer):
id = serializers.UUIDField(required=False)
ip = serializers.CharField(max_length=1024, required=False, allow_blank=True)
class LeakPasswordPSerializer(serializers.ModelSerializer):
def create(self, validated_data):
return LeakPasswords.objects.using('sqlite').create(**validated_data)
def update(self, instance, validated_data):
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save(using='sqlite')
return instance
class Meta:
read_only_fields = ['id']
fields = read_only_fields + ['password']
model = LeakPasswords

View File

@ -8,6 +8,7 @@ from .. import api
app_name = 'common'
router = BulkRouter()
router.register(r'chatai-prompts', api.ChatPromptViewSet, 'chatai-prompt')
router.register(r'leak-passwords', api.LeakPasswordViewSet, 'leak-passwords')
urlpatterns = [
path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'),

View File

@ -17,6 +17,7 @@ from common.utils import (
get_logger,
lazyproperty,
)
from settings.models import LeakPasswords
from users.signals import post_user_change_password
logger = get_logger(__file__)
@ -298,3 +299,8 @@ class AuthMixin:
return ""
password = signer.unsign(secret)
return password
@staticmethod
def check_leak_password(password):
is_exist = LeakPasswords.objects.using('sqlite').filter(password=password).exists()
return is_exist

View File

@ -16,8 +16,8 @@ from authentication.utils import check_user_property_is_correct
from common.const.choices import COUNTRY_CALLING_CODES
from common.utils import FlashMessageUtil, get_object_or_none, random_string
from common.utils.verify_code import SendAndVerifyCodeUtil
from users.serializers import SmsUserSerializer
from users.notifications import ResetPasswordSuccessMsg
from users.serializers import SmsUserSerializer
from ... import forms
from ...models import User
from ...utils import check_password_rules, get_password_check_rules
@ -220,6 +220,11 @@ class UserResetPasswordView(FormView):
form.add_error('new_password', error)
return self.form_invalid(form)
if user.check_leak_password(password):
error = _('Your password is too simple, please change it for security')
form.add_error('new_password', error)
return self.form_invalid(form)
user.reset_password(password)
User.expired_reset_password_token(token)