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 assets.automations.base.manager import BaseManager
from common.const import ConfirmOrIgnore from common.const import ConfirmOrIgnore
from common.decorators import bulk_create_decorator, bulk_update_decorator from common.decorators import bulk_create_decorator, bulk_update_decorator
from settings.models import LeakPasswords
@bulk_create_decorator(AccountRisk) @bulk_create_decorator(AccountRisk)
@ -157,10 +158,8 @@ class CheckLeakHandler(BaseCheckHandler):
if not account.secret: if not account.secret:
return False return False
sql = 'SELECT 1 FROM passwords WHERE password = ? LIMIT 1' is_exist = LeakPasswords.objects.using('sqlite').filter(password=account.secret).exists()
self.cursor.execute(sql, (account.secret,)) return is_exist
leak = self.cursor.fetchone() is not None
return leak
def clean(self): def clean(self):
self.cursor.close() 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', 'PermedAsset', 'PermedAccount', 'MenuPermission',
'Permission', 'TicketSession', 'ApplyLoginTicket', 'Permission', 'TicketSession', 'ApplyLoginTicket',
'ApplyCommandTicket', 'ApplyLoginAssetTicket', 'ApplyCommandTicket', 'ApplyLoginAssetTicket',
'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable' 'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable', 'LeakPasswords'
} }
include_models = {'UserSession'} include_models = {'UserSession'}
for i, app in enumerate(apps.get_models(), 1): 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): def _check_passwd_is_too_simple(cls, user: User, password):
if not user.is_auth_backend_model(): if not user.is_auth_backend_model():
return 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') message = _('Your password is too simple, please change it for security')
url = cls.generate_reset_password_url_with_flash_msg(user, message=message) url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
raise errors.PasswordTooSimple(url) raise errors.PasswordTooSimple(url)

View File

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

View File

@ -705,7 +705,7 @@ class Config(dict):
'FILE_UPLOAD_SIZE_LIMIT_MB': 200, 'FILE_UPLOAD_SIZE_LIMIT_MB': 200,
'TICKET_APPLY_ASSET_SCOPE': 'all', '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 # Ansible Receptor
'RECEPTOR_ENABLED': False, 'RECEPTOR_ENABLED': False,

View File

@ -4,9 +4,11 @@ from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from rest_framework.generics import ListAPIView, CreateAPIView from rest_framework.generics import ListAPIView, CreateAPIView
from rest_framework.views import Response from rest_framework.views import Response
from rest_framework.viewsets import ModelViewSet
from users.utils import LoginIpBlockUtil from users.utils import LoginIpBlockUtil
from ..serializers import SecurityBlockIPSerializer from ..models import LeakPasswords
from ..serializers import SecurityBlockIPSerializer, LeakPasswordPSerializer
class BlockIPSecurityAPI(ListAPIView): class BlockIPSecurityAPI(ListAPIView):
@ -56,3 +58,16 @@ class UnlockIPSecurityAPI(CreateAPIView):
for ip in ips: for ip in ips:
LoginIpBlockUtil(ip).clean_block_if_need() LoginIpBlockUtil(ip).clean_block_if_need()
return Response(status=200) 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): def ready(self):
from . import signal_handlers # noqa from . import signal_handlers # noqa
from . import tasks # 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 json
import os
import shutil
from django.conf import settings from django.conf import settings
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.core.files.uploadedfile import InMemoryUploadedFile 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.db.utils import ProgrammingError, OperationalError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.utils.encoders import JSONEncoder from rest_framework.utils.encoders import JSONEncoder
@ -208,3 +210,38 @@ def get_chatai_data():
data['model'] = settings.DEEPSEEK_MODEL data['model'] = settings.DEEPSEEK_MODEL
return data 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', 'SecurityPasswordRuleSerializer', 'SecuritySessionSerializer',
'SecurityAuthSerializer', 'SecuritySettingSerializer', 'SecurityAuthSerializer', 'SecuritySettingSerializer',
'SecurityLoginLimitSerializer', 'SecurityBasicSerializer', 'SecurityLoginLimitSerializer', 'SecurityBasicSerializer',
'SecurityBlockIPSerializer' 'SecurityBlockIPSerializer', 'LeakPasswordPSerializer'
] ]
from settings.models import LeakPasswords
class SecurityPasswordRuleSerializer(serializers.Serializer): class SecurityPasswordRuleSerializer(serializers.Serializer):
SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField( SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(
@ -269,3 +271,20 @@ class SecuritySettingSerializer(
class SecurityBlockIPSerializer(serializers.Serializer): class SecurityBlockIPSerializer(serializers.Serializer):
id = serializers.UUIDField(required=False) id = serializers.UUIDField(required=False)
ip = serializers.CharField(max_length=1024, required=False, allow_blank=True) 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' app_name = 'common'
router = BulkRouter() router = BulkRouter()
router.register(r'chatai-prompts', api.ChatPromptViewSet, 'chatai-prompt') router.register(r'chatai-prompts', api.ChatPromptViewSet, 'chatai-prompt')
router.register(r'leak-passwords', api.LeakPasswordViewSet, 'leak-passwords')
urlpatterns = [ urlpatterns = [
path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'), path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'),

View File

@ -17,6 +17,7 @@ from common.utils import (
get_logger, get_logger,
lazyproperty, lazyproperty,
) )
from settings.models import LeakPasswords
from users.signals import post_user_change_password from users.signals import post_user_change_password
logger = get_logger(__file__) logger = get_logger(__file__)
@ -298,3 +299,8 @@ class AuthMixin:
return "" return ""
password = signer.unsign(secret) password = signer.unsign(secret)
return password 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.const.choices import COUNTRY_CALLING_CODES
from common.utils import FlashMessageUtil, get_object_or_none, random_string from common.utils import FlashMessageUtil, get_object_or_none, random_string
from common.utils.verify_code import SendAndVerifyCodeUtil from common.utils.verify_code import SendAndVerifyCodeUtil
from users.serializers import SmsUserSerializer
from users.notifications import ResetPasswordSuccessMsg from users.notifications import ResetPasswordSuccessMsg
from users.serializers import SmsUserSerializer
from ... import forms from ... import forms
from ...models import User from ...models import User
from ...utils import check_password_rules, get_password_check_rules from ...utils import check_password_rules, get_password_check_rules
@ -220,6 +220,11 @@ class UserResetPasswordView(FormView):
form.add_error('new_password', error) form.add_error('new_password', error)
return self.form_invalid(form) 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.reset_password(password)
User.expired_reset_password_token(token) User.expired_reset_password_token(token)