Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for authenticated analysis #95

Merged
merged 29 commits into from
Dec 16, 2022
Merged
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cc5c981
Initial TargetCredential model and API operations
pablosnt Nov 27, 2022
bfa1242
Fix model and serializer
pablosnt Nov 29, 2022
45b4e25
Input validation for TargetCredential
pablosnt Nov 29, 2022
c766cae
Get basic authentication value
pablosnt Nov 29, 2022
4915262
Initial unit tests
pablosnt Nov 29, 2022
dee73d8
Fix unit tests for TargetCredential
pablosnt Nov 29, 2022
5c2ef41
Fix input validation for TargetCredential
pablosnt Nov 29, 2022
c3439fa
Refactor UI code for the target port details
pablosnt Nov 29, 2022
3764362
New tab in the popup to handle the target credentials
pablosnt Nov 30, 2022
a03df5a
Fix system credentials validation
pablosnt Nov 30, 2022
01706c3
Add support to filtering target credentials by distinct type
pablosnt Nov 30, 2022
a367975
Use authentication for SMBmap, Dirsearch and JoomScan during executions
pablosnt Dec 10, 2022
9d7b2b2
Replace TargetCredential by TargetAuthentication
pablosnt Dec 10, 2022
d6f235a
Fix references to target_credentials
pablosnt Dec 10, 2022
154d459
Fix input type related to target authentication
pablosnt Dec 10, 2022
edd36cd
Apply authentication in Nikto executions
pablosnt Dec 11, 2022
8034380
Apply authentication in OWASP ZAP executions
pablosnt Dec 11, 2022
33cc53a
Fix reference to OWASP ZAP
pablosnt Dec 11, 2022
22df759
Optimize UX during the credentials configuration
pablosnt Dec 11, 2022
0df961b
Unit tests for executions using target authentication
pablosnt Dec 11, 2022
6303879
Refactoring code using new authentication module and applying authent…
pablosnt Dec 13, 2022
40d5016
Generate migrations and fix some errors
pablosnt Dec 13, 2022
85b591f
Fix some errors and prepare initial unit testing
pablosnt Dec 14, 2022
4bd8d61
Fix error in wordlist input type
pablosnt Dec 15, 2022
3f32951
Fix error obtaining the relationships between the input types
pablosnt Dec 15, 2022
a17b8b8
Optimize code to get authentication
pablosnt Dec 15, 2022
24b9581
Improve unit tests
pablosnt Dec 15, 2022
efa156e
One more unit tests, clean code and fix in arguments syntax
pablosnt Dec 16, 2022
29be2ad
Fix error in arguments with quotes
pablosnt Dec 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
@@ -407,6 +407,14 @@
"line_number": 45,
"is_secret": false
},
{
"type": "Mailchimp Access Key",
"filename": "rekono/testing/data/reports/gitleaks/leaky-repo.json",
"hashed_secret": "4dcd64be183918e45527449e4945c9730f44fc86",
"is_verified": false,
"line_number": 62,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "rekono/testing/data/reports/gitleaks/leaky-repo.json",
@@ -776,7 +784,7 @@
"filename": "rekono/testing/executions/test_base_tool.py",
"hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
"is_verified": false,
"line_number": 294,
"line_number": 306,
"is_secret": false
}
],
@@ -805,6 +813,14 @@
"line_number": 34,
"is_secret": false
},
{
"type": "Mailchimp Access Key",
"filename": "rekono/testing/tools/test_gitleaks.py",
"hashed_secret": "4dcd64be183918e45527449e4945c9730f44fc86",
"is_verified": false,
"line_number": 39,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "rekono/testing/tools/test_gitleaks.py",
@@ -831,5 +847,5 @@
}
]
},
"generated_at": "2022-11-23T22:24:55Z"
"generated_at": "2022-12-16T17:35:49Z"
}
27 changes: 27 additions & 0 deletions rekono/api/serializers.py
Original file line number Diff line number Diff line change
@@ -36,3 +36,30 @@ class RekonoTagSerializerField(TagListSerializerField):
'''Internal serializer field for TagListSerializerField, including API documentation.'''

pass


@extend_schema_field(OpenApiTypes.STR)
class ProtectedStringValueField(serializers.Field):
'''Serializer field to manage protected system values.'''

def to_representation(self, value: str) -> str:
'''Return text value to send to the client.
Args:
value (str): Internal text value
Returns:
str: Text value that contains multiple '*' characters
'''
return '*' * len(value)

def to_internal_value(self, value: str) -> str:
'''Return text value to be stored in database.
Args:
value (str): Text value provided by the client
Returns:
str: Text value to be stored. Save value than the provided one.
'''
return value
1 change: 1 addition & 0 deletions rekono/authentications/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
'''Authentications.'''
7 changes: 7 additions & 0 deletions rekono/authentications/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.contrib import admin

from authentications.models import Authentication

# Register your models here.

admin.site.register(Authentication)
7 changes: 7 additions & 0 deletions rekono/authentications/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.apps import AppConfig


class AuthenticationConfig(AppConfig):
'''Authentication Django application.'''

name = 'authentications'
12 changes: 12 additions & 0 deletions rekono/authentications/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.db import models


class AuthenticationType(models.TextChoices):
'''Supported authentication types.'''

BASIC = 'Basic'
BEARER = 'Bearer'
COOKIE = 'Cookie'
DIGEST = 'Digest'
JWT = 'JWT'
NTLM = 'NTLM'
26 changes: 26 additions & 0 deletions rekono/authentications/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from django_filters import rest_framework
from django_filters.rest_framework.filters import OrderingFilter

from authentications.models import Authentication


class AuthenticationFilter(rest_framework.FilterSet):
'''FilterSet to filter and sort authentications entities.'''

o = OrderingFilter(fields=('target_port', 'name', 'type')) # Ordering fields

class Meta:
model = Authentication
fields = { # Filter fields
'target_port': ['exact'],
'target_port__port': ['exact'],
'target_port__target': ['exact'],
'target_port__target__project': ['exact'],
'target_port__target__project__name': ['exact', 'icontains'],
'target_port__target__project__owner': ['exact'],
'target_port__target__project__owner__username': ['exact', 'icontains'],
'target_port__target__target': ['exact', 'icontains'],
'target_port__target__type': ['exact'],
'name': ['exact', 'icontains'],
'type': ['exact']
}
29 changes: 29 additions & 0 deletions rekono/authentications/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 3.2.16 on 2022-12-13 23:07

from django.db import migrations, models
import django.db.models.deletion
import input_types.base
import security.input_validation


class Migration(migrations.Migration):

initial = True

dependencies = [
('targets', '0002_delete_targetendpoint'),
]

operations = [
migrations.CreateModel(
name='Authentication',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField(max_length=100, validators=[security.input_validation.validate_name])),
('credential', models.TextField(max_length=500, validators=[security.input_validation.validate_credential])),
('type', models.TextField(choices=[('Basic', 'Basic'), ('Bearer', 'Bearer'), ('Cookie', 'Cookie'), ('Digest', 'Digest'), ('JWT', 'Jwt'), ('NTLM', 'Ntlm')], max_length=8)),
('target_port', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='authentication', to='targets.targetport')),
],
bases=(models.Model, input_types.base.BaseInput),
),
]
Empty file.
75 changes: 75 additions & 0 deletions rekono/authentications/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import base64
from typing import Any, Dict

from django.db import models
from input_types.enums import InputKeyword
from input_types.models import BaseInput
from projects.models import Project
from security.input_validation import validate_credential, validate_name
from targets.models import TargetPort
from tools.models import Input

from authentications.enums import AuthenticationType

# Create your models here.


class Authentication(models.Model, BaseInput):
'''Authentication model.'''

# Related target port
target_port = models.OneToOneField(TargetPort, related_name='authentication', on_delete=models.CASCADE)
name = models.TextField(max_length=100, validators=[validate_name]) # Credential name
credential = models.TextField(max_length=500, validators=[validate_credential]) # Credential value
type = models.TextField(max_length=8, choices=AuthenticationType.choices) # Authentication type

def filter(self, input: Input) -> bool:
'''Check if this instance is valid based on input filter.
Args:
input (Input): Tool input whose filter will be applied
Returns:
bool: Indicate if this instance match the input filter or not
'''
if input.filter and input.filter[0] == '!': # Negative filter
return self.type.lower() not in input.filter[1:].split(',') # Check if filter doesn't match the type
# Check if filter matches the type
return not input.filter or self.type.lower() in input.filter.lower().split(',')

def parse(self, accumulated: Dict[str, Any] = {}) -> Dict[str, Any]:
'''Get useful information from this instance to be used in tool execution as argument.
Args:
accumulated (Dict[str, Any], optional): Information from other instances of the same type. Defaults to {}.
Returns:
Dict[str, Any]: Useful information for tool executions, including accumulated if setted
'''
output = self.target_port.parse()
credential = {
InputKeyword.USERNAME.name.lower(): self.name if self.type == AuthenticationType.BASIC else None,
InputKeyword.COOKIE_NAME.name.lower(): self.name if self.type == AuthenticationType.COOKIE else None,
InputKeyword.SECRET.name.lower(): self.credential,
InputKeyword.TOKEN.name.lower(): self.credential if self.type != AuthenticationType.BASIC else base64.b64encode(f'{self.name}:{self.credential}'.encode()).decode(), # noqa: E501
InputKeyword.CREDENTIAL_TYPE.name.lower(): self.type,
InputKeyword.CREDENTIAL_TYPE_LOWER.name.lower(): self.type.lower(),
}
output.update(credential)
return output

def __str__(self) -> str:
'''Instance representation in text format.
Returns:
str: String value that identifies this instance
'''
return f'{self.target_port.__str__()} - {self.name}'

def get_project(self) -> Project:
'''Get the related project for the instance. This will be used for authorization purposes.
Returns:
Project: Related project entity
'''
return self.target_port.target.project
35 changes: 35 additions & 0 deletions rekono/authentications/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Any, Dict

from api.serializers import ProtectedStringValueField
from rest_framework import serializers
from security.input_validation import validate_credential

from authentications.models import Authentication


class AuthenticationSerializer(serializers.ModelSerializer):
'''Serializer to manage authentications via API.'''

credential = ProtectedStringValueField(required=True, allow_null=False) # Credential value in a protected way

class Meta:
'''Serializer metadata.'''

model = Authentication
fields = ('id', 'target_port', 'name', 'credential', 'type') # Authentication fields exposed via API

def validate(self, attrs: Dict[str, Any]) -> Dict[str, Any]:
'''Validate the provided data before use it.
Args:
attrs (Dict[str, Any]): Provided data
Raises:
ValidationError: Raised if provided data is invalid
Returns:
Dict[str, Any]: Data after validation process
'''
attrs = super().validate(attrs)
validate_credential(attrs['credential'])
return attrs
8 changes: 8 additions & 0 deletions rekono/authentications/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from rest_framework.routers import SimpleRouter

from authentications.views import AuthenticationViewSet

router = SimpleRouter()
router.register('authentications', AuthenticationViewSet)

urlpatterns = router.urls
31 changes: 31 additions & 0 deletions rekono/authentications/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from typing import Any, Dict

from django.db.models import QuerySet
from targets.views import TargetViewSet

from authentications.filters import AuthenticationFilter
from authentications.models import Authentication
from authentications.serializers import AuthenticationSerializer

# Create your views here.


class AuthenticationViewSet(TargetViewSet):
'''Authentication ViewSet that includes: get, retrieve, create, and delete features.'''

queryset = Authentication.objects.all().order_by('-id')
serializer_class = AuthenticationSerializer
filterset_class = AuthenticationFilter
search_fields = ['name']
project_members_field = 'target_port__target__project__members'

def get_project_members(self, data: Dict[str, Any]) -> QuerySet:
'''Get project members from serializer validated data.
Args:
data (Dict[str, Any]): Validated data from serializer
Returns:
QuerySet: Project members related to the instance
'''
return data['target_port'].target.project.members.all()
11 changes: 8 additions & 3 deletions rekono/frontend/src/backend/RekonoApi.vue
Original file line number Diff line number Diff line change
@@ -77,11 +77,13 @@ export default {
],
cancellableStatuses: ['Requested', 'Running'],
timeUnits: ['Weeks', 'Days', 'Hours', 'Minutes'],
credentialTypes: ['Basic', 'Bearer', 'Cookie', 'Digest', 'JWT', 'NTLM'],
nameRegex: /^[\wÀ-ÿ\s.\-[\]()]{0,100}$/,
textRegex: /^[\wÀ-ÿ\s.:,+\-'"?¿¡!#%$€[\]()]{0,300}$/,
cveRegex: /^CVE-[0-9]{4}-[0-9]{1,7}$/,
defectDojoKeyRegex: /^[0-9a-z]{40}$/,
telegramTokenRegex: /^[0-9]{10}:[\w-]{35}$/,
cveRegex: /^CVE-\d{4}-\d{1,7}$/,
defectDojoKeyRegex: /^[\da-z]{40}$/,
telegramTokenRegex: /^\d{10}:[\w-]{35}$/,
credentialRegex: /^[\d\w./\-=+,:<>¿?¡!#&$()[\]{}*]{1,500}$/,
telegramBotName: null,
defectDojoUrl: null,
defectDojoEnabled: null
@@ -266,6 +268,9 @@ export default {
validateTelegramToken (value) {
return this.validate(value, this.telegramTokenRegex)
},
validateCredential (value) {
return this.validate(value, this.credentialRegex)
},
validateUrl (value) {
try {
new URL(value);
Loading