class GroupMeta(models.Model):
    group = models.OneToOneField(Group, on_delete=models.CASCADE, primary_key=True)
    owner = models.ForeignKey(User, related_name="group_owner", on_delete=models.CASCADE)
    admins = models.ManyToManyField(User)

    source_required = models.BooleanField(default=True)
    source_options = ArrayField(models.CharField(max_length=75), default=list)

    category_required = models.BooleanField(default=True)
    category_options = ArrayField(models.CharField(max_length=75), default=list)

    nonprivileged_submission_status = models.CharField(max_length=75,
                                                       choices=YaraRule.STATUS_CHOICES,
                                                       default=YaraRule.PENDING_STATUS)

    def save(self, *args, **kwargs):
        self.source_options = list(set(self.source_options))
        self.category_options = list(set(self.category_options))

        super(GroupMeta, self).save(*args, **kwargs) class YaraRule(models.Model):

    ACTIVE_STATUS = 'active'
    INACTIVE_STATUS = 'inactive'
    PENDING_STATUS = 'pending'
    REJECTED_STATUS = 'rejected'

    STATUS_CHOICES = (
        (ACTIVE_STATUS, 'Active'),
        (INACTIVE_STATUS, 'Inactive'),
        (PENDING_STATUS, 'Pending'),
        (REJECTED_STATUS, 'Rejected')
    )

    # Attributes obtained via rule parser
    name = models.CharField(max_length=128)
    strings = JSONField()
    condition = ArrayField(models.TextField())

    tags = ArrayField(models.CharField(max_length=128), default=list)
    scopes = ArrayField(models.CharField(max_length=10), default=list)
    imports = ArrayField(models.CharField(max_length=128), default=list)
    metadata = HStoreField(default=dict)

    # Listing of rule dependencies
    dependencies = ArrayField(models.TextField(), default=list)

    # Hash value of rule strings and condition to identify logical uniqueness
    logic_hash = models.CharField(max_length=64)

    # Which group owns this rule
    owner = models.ForeignKey(Group, editable=False, on_delete=models.CASCADE,
                              related_name="rule_owner")

    # Who submitted this rule
    submitter = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, 
                                  related_name="rule_submitter")

    # Where did the rule come from
    source = models.CharField(max_length=75, blank=True)

    # What should this rule be run against
    category = models.CharField(max_length=75, blank=True)

    # Status description of the rule (active / inactive)
    status = models.CharField(max_length=75,
                              choices=STATUS_CHOICES,
                              default=ACTIVE_STATUS)

    modified = models.DateTimeField(auto_now=True)
    created = models.DateTimeField(auto_now_add=True, editable=False)

    # Instantiate custom manager
    objects = YaraRuleManager()

    def save(self, *args, **kwargs):
        super(YaraRule, self).save(*args, **kwargs)

    def format_rule(self):
        raw_rule = {}

        raw_rule['rule_name'] = self.name
        raw_rule['tags'] = self.tags
        raw_rule['imports'] = self.imports
        raw_rule['metadata'] = self.metadata
        raw_rule['strings'] = self.strings
        raw_rule['condition_terms'] = self.condition
        raw_rule['scopes'] = self.scopes

        formatted_rule = Plyara.rebuild_yara_rule(raw_rule)
        return formatted_rule

    def __str__(self):
        return self.name class YaraRuleComment(models.Model):
    content = models.TextField()
    poster = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    rule = models.ForeignKey(YaraRule, on_delete=models.CASCADE)
    modified = models.DateTimeField(auto_now=True)
    created = models.DateTimeField(auto_now_add=True, editable=False)

    # Instantiate custom manager
    objects = YaraRuleCommentManager() def generate_test_user(username=None, password=None):
    if username is not None:
        username = username
    else:
        username = 'TEST_USER'

    if password is not None:
        password = password
    else:
        password = 'TEST_PASSWORD'

    return User.objects.create_user(username=username,
                                    password=password) class RulesetsListingView(APIView):
    """
    Lists all application rulesets.
    """
    permission_classes = [IsAuthenticated]

    def get(self, request):
        groups = request.user.groups.all()
        return Response([group.name for group in groups]) class RulesetView(CreateAPIView):
    """
    Create new rule.
    """
    serializer_class = YaraRuleSerializer
    permission_classes = [IsGroupAdminOrMemberAddMethod] class RulesetStatsView(APIView):
    """
    Retrieve stats on a set of rules.
    """
    permission_classes = [IsGroupMember]
    serializer_class = YaraRuleStatsSerializer

    def get(self, request, group_name):
        response_content = {}
        group_context = get_group_or_404(group_name)
        serializer_context = {'group_context': group_context}
        queryset = get_queryset(group_context, query_params=request.query_params)

        # Return only the stats that are being specifically filtered for
        if 'filter' in request.query_params:
            fields = set(request.query_params.getlist('filter'))
            serializer = self.serializer_class(queryset,
                                               fields=fields,
                                               context=serializer_context)

        # Return all available stats
        else:
            serializer = self.serializer_class(queryset, context=serializer_context)

        return Response(serializer.data) class RulesetSearchView(ListAPIView):
    """
    Search for rules that match the provided criteria.
    """
    paginator = RuleSetPagination()
    permission_classes = [IsGroupMember]
    filter_class = YaraRuleFilter

    def get_queryset(self):
        group_context = get_group_or_404(self.kwargs['group_name'])
        queryset = YaraRule.objects.filter(owner=group_context)
        return queryset

    def get_serializer_class(self):
        return YaraRuleSerializer

    def filter_queryset(self, queryset):
        return self.filter_class(self.request.query_params, queryset=queryset).qs.order_by('name') class RulesetExportView(APIView):
    """
    Export rules
    """
    permission_classes = [IsGroupMember]

    def get(self, request, group_name):
        # Specify metadata of file object
        file_meta = 'attachment; filename="RuleExport.yara"'

        # Filter based on query params and float dependent rules towards bottom
        group_context = get_group_or_404(group_name)
        queryset = get_queryset(group_context, query_params=request.query_params)

        # Build rule file
        rule_file = build_yarafile(queryset)

        response = HttpResponse(content=rule_file.getvalue())
        response['Content-Type'] = 'application/text'
        response['Content-Disposition'] = file_meta

        return response class RulesetBulkEditView(APIView):
    """
    post:
    Add rules in bulk to specified ruleset.
    patch:
    Edit rules in bulk to specified ruleset.
    delete:
    Delete rules in bulk to specified ruleset.
    """
    permission_classes = [IsGroupAdminOrMemberAddMethod]

    def post(self, request, group_name):
        response_content= {'errors': [],
                           'warnings': [],
                           'rule_upload_count': 0,
                           'rule_collision_count': 0}

        submitter = request.user
        group_context = get_group_or_404(group_name)

        # Retrieve submitted yara rule content
        submissions = request.data.getlist('rule_content')

        # Only owners and admins can specifically select the status
        if group_admin(request):
            status = request.data.get('status')

            if status not in [YaraRule.ACTIVE_STATUS, YaraRule.INACTIVE_STATUS, YaraRule.PENDING_STATUS]:
                status = YaraRule.INACTIVE_STATUS
        # Rules from others are automatically put into nonprivileged status
        else:
            status = group_context.groupmeta.nonprivileged_submission_status

        # Check for source and category
        source = request.data.get('source', '')
        category = request.data.get('category', '')

        # Check for in-line modifications
        add_tags = request.data.get('add_tags', None)
        prepend_name = request.data.get('prepend_name', None)
        append_name = request.data.get('append_name', None)

        add_metadata = {metakey[13:] : metavalue
                        for metakey, metavalue in request.data.items()
                        if metakey.startswith('set_metadata_')}

        # Process each submission
        for raw_submission in submissions:
            submission_results = parse_rule_submission(raw_submission)
            # Inspect the submission results
            parsed_rules = submission_results['parsed_rules']
            parsing_error = submission_results['parser_error']
            # Identify any parsing errors that occur
            if parsing_error:
                response_content['errors'].append(parsing_error)
            else:
                # Save successfully parsed rules
                save_results = YaraRule.objects.process_parsed_rules(parsed_rules,
                                                                     source, category,
                                                                     submitter, group_context,
                                                                     status=status,
                                                                     add_tags=add_tags,
                                                                     add_metadata=add_metadata,
                                                                     prepend_name=prepend_name,
                                                                     append_name=append_name)

                response_content['errors'].extend(save_results['errors'])
                response_content['warnings'].extend(save_results['warnings'])
                response_content['rule_upload_count'] += save_results['rule_upload_count']
                response_content['rule_collision_count'] += save_results['rule_collision_count']

        return Response(response_content)

    def patch(self, request, group_name):
        group_context = get_group_or_404(group_name)

        if request.query_params:
            # Filter based on query params
            queryset = get_queryset(group_context, query_params=request.query_params)
        else:
            # Do not allow method to occur by setting the queryset to none
            queryset = YaraRule.objects.none()

        rule_count = queryset.count()
        rule_names = list(queryset.values_list('name', flat=True).distinct())

        update_feedback = queryset.bulk_update(request.data)

        response_content = {'modified_rule_count': rule_count,
                            'modified_rule_names': rule_names,
                            'errors': update_feedback['errors'],
                            'warnings': update_feedback['warnings']}

        return Response(response_content)

    def delete(self, request, group_name):
        group_context = get_group_or_404(group_name)

        if request.query_params:
            # Filter based on query params
            queryset = get_queryset(group_context, query_params=request.query_params)
        else:
            # Do not allow method to occur by setting the queryset to none
            queryset = YaraRule.objects.none()

        rule_count = queryset.count()
        rule_names = list(queryset.values_list('name', flat=True).distinct())

        queryset.delete()

        response_content = {'deleted_rule_count': rule_count,
                            'deleted_rule_names': rule_names}

        return Response(response_content) class RulesetDeconflictView(APIView):
    """
    Deconflict / Merge Logic Collisions
    """
    permission_classes = [IsGroupAdminOrMemberReadOnly]

    def patch(self, request, group_name):
        group_context = get_group_or_404(group_name)

        if request.query_params:
            # Filter and deconflict based on query params
            queryset = get_queryset(group_context, query_params=request.query_params)
        else:
            # Deconflict all
            queryset = YaraRule.objects.filter(owner=group_context)

        response_content = queryset.deconflict_logic()

        return Response(response_content) class RuleDetailsView(APIView):
    """
    get:
    Retrieve specified rule.
    put:
    Replace specified rule.
    patch:
    Update specified rule.
    delete:
    Delete specified rule.
    """
    serializer_class = YaraRuleSerializer
    permission_classes = [IsGroupAdminOrMemberReadOnly]

    def get(self, request, group_name, rule_pk):
        group_context = get_group_or_404(group_name)
        queryset = get_queryset(group_context)
        rule = get_object_or_404(queryset, pk=rule_pk)
        serializer = self.serializer_class(rule)
        return Response(serializer.data)

    def put(self, request, group_name, rule_pk):
        group_context = get_group_or_404(group_name)
        queryset = get_queryset(group_context)
        rule = get_object_or_404(queryset, pk=rule_pk)

        serializer = self.serializer_class(rule, data=request.data)

        if serializer.is_valid():
            serializer.save()

        return Response(serializer.data)

    def patch(self, request, group_name, rule_pk):
        group_context = get_group_or_404(group_name)
        queryset = get_queryset(group_context)
        rule = get_object_or_404(queryset, pk=rule_pk)
        serializer = self.serializer_class(rule, data=request.data, partial=True)

        if serializer.is_valid():
            serializer.save()

        return Response(serializer.data)

    def delete(self, request, group_name, rule_pk):
        group_context = get_group_or_404(group_name)
        queryset = get_queryset(group_context)
        rule = get_object_or_404(queryset, pk=rule_pk)
        rule.delete()
        return Response(status=status.HTTP_204_NO_CONTENT) class RuleTagsView(APIView):
    """
    Delete tag from specified rule.
    """
    permission_classes = [IsGroupAdminOrMemberReadOnly]

    def delete(self, request, group_name, rule_pk, tag):
        group_context = get_group_or_404(group_name)
        queryset = get_queryset(group_context)
        rule = get_object_or_404(queryset, pk=rule_pk)

        if tag and tag in rule.tags:
            rule.tags.remove(tag)
            rule.save(update_fields=['tags'])

        serializer = YaraRuleSerializer(rule)
        return Response(serializer.data) class RuleMetadataView(APIView):
    """
    Delete metadata from specified rule.
    """
    permission_classes = [IsGroupAdminOrMemberReadOnly]

    def delete(self, request, group_name, rule_pk, metakey):
        group_context = get_group_or_404(group_name)
        queryset = get_queryset(group_context)
        rule = get_object_or_404(queryset, pk=rule_pk)

        if metakey and metakey in rule.metadata:
            rule.metadata.pop(metakey)
            rule.save(update_fields=['metadata'])

        serializer = YaraRuleSerializer(rule)
        return Response(serializer.data) class RuleCommentsView(APIView):
    """
    get:
    List comments belonging to specified rule.
    post:
    Add comment to specified rule.
    """
    serializer_class = YaraRuleCommentSerializer
    permission_classes = [IsGroupAdminOrMemberReadOnly]

    def get(self, request, group_name, rule_pk):
        group_context = get_group_or_404(group_name)
        queryset = get_queryset(group_context)
        rule = get_object_or_404(queryset, pk=rule_pk)
        comments = rule.yararulecomment_set.all()
        serializer = self.serializer_class(comments, many=True)
        return Response(serializer.data)

    def post(self, request, group_name, rule_pk):
        group_context = get_group_or_404(group_name)
        queryset = get_queryset(group_context)
        rule = get_object_or_404(queryset, pk=rule_pk)

        serializer = self.serializer_class(data=request.data,
                                           context={'request': request,
                                                    'view': self})

        if serializer.is_valid():
            serializer.save()

        return Response(serializer.data, status=status.HTTP_201_CREATED) class RuleCommentDetailsView(APIView):
    """
    get:
    Retrieve specific rule comment.
    put:
    Update rule comment.
    delete:
    Delete rule comment.
    """
    serializer_class = YaraRuleCommentSerializer
    permission_classes = [IsGroupAdminOrMemberReadOnly]

    def get(self, request, group_name, rule_pk, comment_pk):
        group_context = get_group_or_404(group_name)
        queryset = get_queryset(group_context)
        rule = get_object_or_404(queryset, pk=rule_pk)
        comment = get_object_or_404(rule.yararulecomment_set.all(), pk=comment_pk)

        serializer = self.serializer_class(comment)
        return Response(serializer.data)

    def put(self, request, group_name, rule_pk, comment_pk):
        group_context = get_group_or_404(group_name)
        queryset = get_queryset(group_context)
        rule = get_object_or_404(queryset, pk=rule_pk)
        comment = get_object_or_404(rule.yararulecomment_set.all(), pk=comment_pk)

        serializer = self.serializer_class(comment, data=request.data)

        if serializer.is_valid():
            serializer.save()

        return Response(serializer.data)

    def delete(self, request, group_name, rule_pk, comment_pk):
        group_context = get_group_or_404(group_name)
        queryset = get_queryset(group_context)
        rule = get_object_or_404(queryset, pk=rule_pk)
        comment = get_object_or_404(rule.yararulecomment_set.all(), pk=comment_pk)
        comment.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
from urllib.parse import urlencode
from django.test import TestCase
from django.urls import reverse, resolve
from django.contrib.auth.models import Group
from rest_framework.test import APIRequestFactory, force_authenticate
from core.models import GroupMeta
from .models import YaraRule, YaraRuleComment
from .testing_core import generate_test_user
from .REST_views import (RulesetsListingView,
                         RulesetView,
                         RulesetStatsView,
                         RulesetSearchView,
                         RulesetExportView,
                         RulesetBulkEditView,
                         RulesetDeconflictView,
                         RuleDetailsView,
                         RuleTagsView,
                         RuleMetadataView,
                         RuleCommentsView,
                         RuleCommentDetailsView)

        self.assertEqual(response.status_code, 403)


class RulesetDeconflictViewTestCase(TestCase):

    def setUp(self):
        self.factory = APIRequestFactory()
        self.view = RulesetDeconflictView.as_view()

    @classmethod
    def setUpTestData(cls):
        cls.user1 = generate_test_user(username='VIEW_TESTER_0001')
        cls.user2 = generate_test_user(username='VIEW_TESTER_0002')
        
        cls.group1 = cls.user1.groups.get()
        cls.group2 = cls.user2.groups.get()

        # Add user2 to group1
        cls.user2.groups.add(cls.group1)

    def test_admin_patch_request(self):
        url = reverse('ruleset-deconflict', kwargs={'group_name': self.group1.name})
        request = self.factory.patch(url)
        request.resolver_match = resolve(url)
        force_authenticate(request, user=self.user1)
        response = self.view(request, group_name=self.group1.name)

        self.assertEqual(response.status_code, 200)

    def test_nonadmin_patch_request(self):
        url = reverse('ruleset-deconflict', kwargs={'group_name': self.group1.name})
        request = self.factory.patch(url)
        request.resolver_match = resolve(url)
        force_authenticate(request, user=self.user2)
        response = self.view(request, group_name=self.group1.name)

        self.assertEqual(response.status_code, 403)

    def test_unauthenticated_patch_request(self):
        url = reverse('ruleset-deconflict', kwargs={'group_name': self.group1.name})
        request = self.factory.patch(url)
        request.resolver_match = resolve(url)
        response = self.view(request, group_name=self.group1.name)

        self.assertEqual(response.status_code, 403)


class RuleDetailsViewTestCase(TestCase):

    def setUp(self):
        self.factory = APIRequestFactory()
        self.view = RuleDetailsView.as_view()

        self.rule = YaraRule.objects.create(name='dummy',
                                            strings={},
                                            condition=[],
                                            tags=[],
                                            scopes=[],
                                            imports=[],
                                            metadata={},
                                            dependencies=[],
                                            logic_hash='',
                                            owner=self.group1,
                                            submitter=self.user1,
                                            source='',
                                            category='',
                                            status=YaraRule.ACTIVE_STATUS)

        self.rule.save()

    @classmethod
    def setUpTestData(cls):
        cls.user1 = generate_test_user(username='VIEW_TESTER_0001')
        cls.user2 = generate_test_user(username='VIEW_TESTER_0002')
        
        cls.group1 = cls.user1.groups.get()
        cls.group2 = cls.user2.groups.get()

        # Add user2 to group1
        cls.user2.groups.add(cls.group1)

    def test_member_get_request(self):
        url = reverse('rule-details', kwargs={'group_name': self.group1.name, 'rule_pk': self.rule.id})
        request = self.factory.get(url)
        request.resolver_match = resolve(url)
        force_authenticate(request, user=self.user2)
        response = self.view(request, group_name=self.group1.name, rule_pk=self.rule.id)
        
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['name'], self.rule.name)

    def test_unauthenticated_get_request(self):
        url = reverse('rule-details', kwargs={'group_name': self.group1.name, 'rule_pk': self.rule.id})
        request = self.factory.get(url)
        request.resolver_match = resolve(url)
        response = self.view(request, group_name=self.group1.name, rule_pk=self.rule.id)
        
        self.assertEqual(response.status_code, 403)

    def test_admin_put_request(self):
        url = reverse('rule-details', kwargs={'group_name': self.group1.name, 'rule_pk': self.rule.id})
        request = self.factory.put(url, {'source': '',
                                         'category': '',
                                         'rule_content': 'rule replaced { condition: false }',
                                         'status': YaraRule.INACTIVE_STATUS})

        request.resolver_match = resolve(url)
        force_authenticate(request, user=self.user1)
        response = self.view(request, group_name=self.group1.name, rule_pk=self.rule.id)

        self.rule.refresh_from_db()
        self.assertEqual(response.status_code, 200)
        self.assertTrue(self.rule.name == 'replaced')

    def test_nonadmin_put_request(self):
        url = reverse('rule-details', kwargs={'group_name': self.group1.name, 'rule_pk': self.rule.id})
        request = self.factory.put(url, {'source': '',
                                         'category': '',
                                         'rule_content': 'rule replaced { condition: false }',

                                         'status': YaraRule.INACTIVE_STATUS})