secator 0.12.0__py3-none-any.whl → 0.13.0__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 secator might be problematic. Click here for more details.

Files changed (44) hide show
  1. secator/cli.py +174 -65
  2. secator/configs/workflows/host_recon.yaml +11 -2
  3. secator/configs/workflows/port_scan.yaml +39 -0
  4. secator/configs/workflows/url_dirsearch.yaml +5 -0
  5. secator/definitions.py +0 -4
  6. secator/installer.py +29 -15
  7. secator/runners/_base.py +2 -1
  8. secator/runners/_helpers.py +13 -2
  9. secator/runners/command.py +2 -1
  10. secator/tasks/_categories.py +2 -2
  11. secator/tasks/arjun.py +2 -1
  12. secator/tasks/bbot.py +30 -4
  13. secator/tasks/bup.py +2 -1
  14. secator/tasks/cariddi.py +15 -3
  15. secator/tasks/dalfox.py +2 -1
  16. secator/tasks/dirsearch.py +1 -1
  17. secator/tasks/dnsx.py +2 -1
  18. secator/tasks/dnsxbrute.py +2 -1
  19. secator/tasks/feroxbuster.py +3 -2
  20. secator/tasks/ffuf.py +2 -1
  21. secator/tasks/gau.py +2 -1
  22. secator/tasks/gitleaks.py +4 -3
  23. secator/tasks/gospider.py +3 -2
  24. secator/tasks/grype.py +1 -0
  25. secator/tasks/h8mail.py +2 -1
  26. secator/tasks/httpx.py +3 -2
  27. secator/tasks/katana.py +4 -3
  28. secator/tasks/maigret.py +1 -1
  29. secator/tasks/mapcidr.py +2 -1
  30. secator/tasks/msfconsole.py +4 -3
  31. secator/tasks/naabu.py +4 -2
  32. secator/tasks/nuclei.py +15 -9
  33. secator/tasks/searchsploit.py +3 -2
  34. secator/tasks/subfinder.py +2 -1
  35. secator/tasks/testssl.py +4 -3
  36. secator/tasks/trivy.py +2 -2
  37. secator/tasks/wafw00f.py +2 -1
  38. secator/tasks/wpprobe.py +2 -1
  39. secator/tasks/wpscan.py +6 -3
  40. {secator-0.12.0.dist-info → secator-0.13.0.dist-info}/METADATA +1 -1
  41. {secator-0.12.0.dist-info → secator-0.13.0.dist-info}/RECORD +44 -43
  42. {secator-0.12.0.dist-info → secator-0.13.0.dist-info}/WHEEL +0 -0
  43. {secator-0.12.0.dist-info → secator-0.13.0.dist-info}/entry_points.txt +0 -0
  44. {secator-0.12.0.dist-info → secator-0.13.0.dist-info}/licenses/LICENSE +0 -0
secator/cli.py CHANGED
@@ -37,7 +37,7 @@ ALL_TASKS = discover_tasks()
37
37
  ALL_WORKFLOWS = [t for t in TEMPLATES if t.type == 'workflow']
38
38
  ALL_SCANS = [t for t in TEMPLATES if t.type == 'scan']
39
39
  FINDING_TYPES_LOWER = [c.__name__.lower() for c in FINDING_TYPES]
40
- CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
40
+ CONTEXT_SETTINGS = dict(help_option_names=['-h', '-help', '--help'])
41
41
 
42
42
 
43
43
  #-----#
@@ -46,15 +46,16 @@ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
46
46
 
47
47
 
48
48
  @click.group(cls=OrderedGroup, invoke_without_command=True, context_settings=CONTEXT_SETTINGS)
49
- @click.option('--version', '-version', is_flag=True, default=False)
49
+ @click.option('--version', '-version', '-v', is_flag=True, default=False)
50
+ @click.option('--quiet', '-quiet', '-q', is_flag=True, default=False)
50
51
  @click.pass_context
51
- def cli(ctx, version):
52
+ def cli(ctx, version, quiet):
52
53
  """Secator CLI."""
53
54
  ctx.obj = {
54
55
  'piped_input': S_ISFIFO(os.fstat(0).st_mode),
55
56
  'piped_output': not sys.stdout.isatty()
56
57
  }
57
- if not ctx.obj['piped_output']:
58
+ if not ctx.obj['piped_output'] and not quiet:
58
59
  console.print(ASCII, highlight=False)
59
60
  if ctx.invoked_subcommand is None:
60
61
  if version:
@@ -67,11 +68,16 @@ def cli(ctx, version):
67
68
  # TASK #
68
69
  #------#
69
70
 
70
- @cli.group(aliases=['x', 't'])
71
+ @cli.group(aliases=['x', 't'], invoke_without_command=True)
72
+ @click.option('--list', '-list', is_flag=True, default=False)
71
73
  @click.pass_context
72
- def task(ctx):
74
+ def task(ctx, list=False):
73
75
  """Run a task."""
74
- pass
76
+ if list:
77
+ print("\n".join(sorted([t.__name__ for t in ALL_TASKS])))
78
+ return
79
+ if ctx.invoked_subcommand is None:
80
+ ctx.get_help()
75
81
 
76
82
 
77
83
  for cls in ALL_TASKS:
@@ -83,11 +89,16 @@ for cls in ALL_TASKS:
83
89
  #----------#
84
90
 
85
91
 
86
- @cli.group(cls=OrderedGroup, aliases=['w'])
92
+ @cli.group(cls=OrderedGroup, aliases=['w'], invoke_without_command=True)
93
+ @click.option('--list', '-list', is_flag=True, default=False)
87
94
  @click.pass_context
88
- def workflow(ctx):
95
+ def workflow(ctx, list=False):
89
96
  """Run a workflow."""
90
- pass
97
+ if list:
98
+ print("\n".join(sorted([t.name for t in ALL_WORKFLOWS])))
99
+ return
100
+ if ctx.invoked_subcommand is None:
101
+ ctx.get_help()
91
102
 
92
103
 
93
104
  for config in sorted(ALL_WORKFLOWS, key=lambda x: x['name']):
@@ -98,11 +109,16 @@ for config in sorted(ALL_WORKFLOWS, key=lambda x: x['name']):
98
109
  # SCAN #
99
110
  #------#
100
111
 
101
- @cli.group(cls=OrderedGroup, aliases=['s'])
112
+ @cli.group(cls=OrderedGroup, aliases=['s'], invoke_without_command=True)
113
+ @click.option('--list', '-list', is_flag=True, default=False)
102
114
  @click.pass_context
103
- def scan(ctx):
115
+ def scan(ctx, list=False):
104
116
  """Run a scan."""
105
- pass
117
+ if list:
118
+ print("\n".join(sorted([t.name for t in ALL_SCANS])))
119
+ return
120
+ if ctx.invoked_subcommand is None:
121
+ ctx.get_help()
106
122
 
107
123
 
108
124
  for config in sorted(ALL_SCANS, key=lambda x: x['name']):
@@ -860,13 +876,21 @@ def health(json, debug, strict):
860
876
  import json as _json
861
877
  print(_json.dumps(status))
862
878
 
879
+ # Print errors and warnings
880
+ error = False
881
+ for tool, info in status['tools'].items():
882
+ if not info['installed']:
883
+ console.print(Warning(message=f'{tool} is not installed.'))
884
+ error = True
885
+ elif info['outdated']:
886
+ message = (
887
+ f'{tool} is outdated (current:{info["version"]}, latest:{info["latest_version"]}).'
888
+ f' Run `secator install tools {tool}` to update it.'
889
+ )
890
+ console.print(Warning(message=message))
891
+
863
892
  # Strict mode
864
893
  if strict:
865
- error = False
866
- for tool, info in status['tools'].items():
867
- if not info['installed']:
868
- console.print(Error(message=f'{tool} is not installed.'))
869
- error = True
870
894
  if error:
871
895
  sys.exit(1)
872
896
  console.print(Info(message='Strict healthcheck passed !'))
@@ -1061,16 +1085,27 @@ def install_tools(cmds, cleanup, fail_fast):
1061
1085
  if CONFIG.offline_mode:
1062
1086
  console.print(Error(message='Cannot run this command in offline mode.'))
1063
1087
  return
1088
+ tools = []
1064
1089
  if cmds is not None:
1065
1090
  cmds = cmds.split(',')
1066
- tools = [cls for cls in ALL_TASKS if cls.__name__ in cmds]
1091
+ for cmd in cmds:
1092
+ if '==' in cmd:
1093
+ cmd, version = tuple(cmd.split('=='))
1094
+ else:
1095
+ cmd, version = cmd, None
1096
+ cls = next((cls for cls in ALL_TASKS if cls.__name__ == cmd), None)
1097
+ if cls:
1098
+ if version:
1099
+ cls.install_version = version
1100
+ tools.append(cls)
1101
+ else:
1102
+ console.print(Warning(message=f'Tool {cmd} is not supported or inexistent.'))
1067
1103
  else:
1068
1104
  tools = ALL_TASKS
1069
1105
  tools.sort(key=lambda x: x.__name__)
1070
1106
  return_code = 0
1071
1107
  if not tools:
1072
- cmd_str = ' '.join(cmds)
1073
- console.print(Error(message=f'No tools found for {cmd_str}.'))
1108
+ console.print(Error(message='No tools found for installing.'))
1074
1109
  return
1075
1110
  for ix, cls in enumerate(tools):
1076
1111
  # with console.status(f'[bold yellow][{ix + 1}/{len(tools)}] Installing {cls.__name__} ...'):
@@ -1140,7 +1175,7 @@ def update(all):
1140
1175
  cmd = cls.cmd.split(' ')[0]
1141
1176
  version_flag = cls.get_version_flag()
1142
1177
  info = get_version_info(cmd, version_flag, cls.install_github_handle)
1143
- if not info['installed'] or info['status'] == 'outdated' or not info['latest_version']:
1178
+ if not info['installed'] or info['outdated'] or not info['latest_version']:
1144
1179
  # with console.status(f'[bold yellow]Installing {cls.__name__} ...'):
1145
1180
  status = ToolInstaller.install(cls)
1146
1181
  if not status.is_ok():
@@ -1375,73 +1410,147 @@ def performance(tasks, workflows, scans, test):
1375
1410
  @test.command()
1376
1411
  @click.argument('name', type=str)
1377
1412
  @click.option('--verbose', '-v', is_flag=True, default=False, help='Print verbose output')
1378
- def task(name, verbose):
1379
- """Test task."""
1413
+ @click.option('--check', '-c', is_flag=True, default=False, help='Check task semantics only (no unit + integration tests)') # noqa: E501
1414
+ def task(name, verbose, check):
1415
+ """Test a single task for semantics errors, and run unit + integration tests."""
1416
+ console.print(f'[bold gold3]:wrench: Testing task {name} ...[/]')
1380
1417
  task = [task for task in ALL_TASKS if task.__name__ == name]
1381
1418
  warnings = []
1419
+ errors = []
1382
1420
  exit_code = 0
1383
1421
 
1384
1422
  # Check if task is correctly registered
1385
- check_error(task, 'Check task is registered', 'Task is not registered. Make sure there is no syntax errors in the task class definition.', warnings) # noqa: E501
1386
1423
  task = task[0]
1387
1424
  task_name = task.__name__
1388
1425
 
1389
1426
  # Run install
1390
- console.print(f'\n[bold gold3]:wrench: Running install tests for task {name} ...[/]') if verbose else None
1391
1427
  cmd = f'secator install tools {task_name}'
1392
1428
  ret_code = Command.execute(cmd, name='install', quiet=not verbose, cwd=ROOT_FOLDER)
1393
1429
  version_info = task.get_version_info()
1394
- check_error(version_info['installed'], 'Check task is installed', 'Failed to install command. Fix your installation command.', warnings) # noqa: E501
1395
- check_error(any(cmd for cmd in [task.install_cmd, task.install_github_handle]), 'Check task installation command is defined', 'Task has no installation command. Please define a `install_cmd` or `install_github_handle` class attribute.', warnings) # noqa: E501
1396
- check_error(version_info['version'], 'Check task version can be fetched', 'Failed to detect version info. Fix your `version_flag` class attribute.', warnings) # noqa: E501
1430
+ if verbose:
1431
+ console.print(f'Version info:\n{version_info}')
1432
+ status = version_info['status']
1433
+ check_test(
1434
+ version_info['installed'],
1435
+ 'Check task is installed',
1436
+ 'Failed to install command. Fix your installation command.',
1437
+ errors
1438
+ )
1439
+ check_test(
1440
+ any(cmd for cmd in [task.install_cmd, task.install_github_handle]),
1441
+ 'Check task installation command is defined',
1442
+ 'Task has no installation command. Please define one or more of the following class attributes: `install_pre`, `install_cmd`, `install_post`, `install_github_handle`.', # noqa: E501
1443
+ errors
1444
+ )
1445
+ check_test(
1446
+ version_info['version'],
1447
+ 'Check task version can be fetched',
1448
+ 'Failed to detect current version. Consider updating your `version_flag` class attribute.',
1449
+ warnings,
1450
+ warn=True
1451
+ )
1452
+ check_test(
1453
+ status != 'latest unknown',
1454
+ 'Check latest version',
1455
+ 'Failed to detect latest version.',
1456
+ warnings,
1457
+ warn=True
1458
+ )
1459
+ check_test(
1460
+ not version_info['outdated'],
1461
+ 'Check task version is up to date',
1462
+ f'Task is not up to date (current version: {version_info["version"]}, latest: {version_info["latest_version"]}). Consider updating your `install_version` class attribute.', # noqa: E501
1463
+ warnings,
1464
+ warn=True
1465
+ )
1397
1466
 
1398
1467
  # Run task-specific tests
1399
- console.print(f'\n[bold gold3]:wrench: Running task-specific tests for {name} ...[/]') if verbose else None
1400
- check_error(task.__doc__, 'Check task description is set (cls.__doc__)', 'Task has no description (class docstring).', warnings) # noqa: E501
1401
- check_error(task.cmd, 'Check task command is set (cls.cmd)', 'Task has no cmd attribute.', warnings)
1402
- check_error(task.input_type, 'Check task input type is set (cls.input_type)', 'Task has no input_type attribute.', warnings) # noqa: E501
1403
- check_error(task.output_types, 'Check task output types is set (cls.output_types)', 'Task has no output_types attribute.', warnings) # noqa: E501
1404
-
1405
- # Print all warnings
1406
- exit_code = 1 if len(warnings) > 0 else 0
1407
- if exit_code == 1:
1408
- console.print()
1409
- console.print("[bold red]Issues:[/]")
1410
- for warning in warnings:
1411
- console.print(warning)
1412
- console.print()
1413
- console.print(Info(message=f'Skipping unit and integration tests for {name} due to previous errors.'))
1414
- console.print(Error(message=f'Task {name} tests failed. Please fix the issues above before making a PR.'))
1415
- sys.exit(exit_code)
1416
-
1417
- # Run unit tests
1418
- console.print(f'\n[bold gold3]:wrench: Running unit tests for {name} ...[/]') if verbose else None
1419
- cmd = f'secator test unit --tasks {name}'
1420
- ret_code = run_test(cmd, exit=False, verbose=verbose)
1421
- check_error(ret_code == 0, 'Check unit tests pass', 'Unit tests failed.', warnings)
1422
-
1423
- # Run integration tests
1424
- console.print(f'\n[bold gold3]:wrench: Running integration tests for {name} ...[/]') if verbose else None
1425
- cmd = f'secator test integration --tasks {name}'
1426
- ret_code = run_test(cmd, exit=False, verbose=verbose)
1427
- check_error(ret_code == 0, 'Check integration tests pass', 'Integration tests failed.', warnings)
1468
+ check_test(
1469
+ task.__doc__,
1470
+ 'Check task description is set (cls.__doc__)',
1471
+ 'Task has no description (class docstring).',
1472
+ errors
1473
+ )
1474
+ check_test(
1475
+ task.cmd,
1476
+ 'Check task command is set (cls.cmd)',
1477
+ 'Task has no cmd attribute.',
1478
+ errors
1479
+ )
1480
+ check_test(
1481
+ task.input_type,
1482
+ 'Check task input type is set (cls.input_type)',
1483
+ 'Task has no input_type attribute.',
1484
+ warnings,
1485
+ warn=True
1486
+ )
1487
+ check_test(
1488
+ task.output_types,
1489
+ 'Check task output types is set (cls.output_types)',
1490
+ 'Task has no output_types attribute. Consider setting some so that secator can load your task outputs.',
1491
+ warnings,
1492
+ warn=True
1493
+ )
1494
+ check_test(
1495
+ task.install_version,
1496
+ 'Check task install_version is set (cls.install_version)',
1497
+ 'Task has no install_version attribute. Consider setting it to pin the tool version and ensure it does not break in the future.', # noqa: E501
1498
+ warnings,
1499
+ warn=True
1500
+ )
1501
+
1502
+ if not check:
1503
+
1504
+ # Run unit tests
1505
+ cmd = f'secator test unit --tasks {name}'
1506
+ ret_code = run_test(cmd, exit=False, verbose=verbose)
1507
+ check_test(
1508
+ ret_code == 0,
1509
+ 'Check unit tests pass',
1510
+ 'Unit tests failed.',
1511
+ errors
1512
+ )
1513
+
1514
+ # Run integration tests
1515
+ cmd = f'secator test integration --tasks {name}'
1516
+ ret_code = run_test(cmd, exit=False, verbose=verbose)
1517
+ check_test(
1518
+ ret_code == 0,
1519
+ 'Check integration tests pass',
1520
+ 'Integration tests failed.',
1521
+ errors
1522
+ )
1428
1523
 
1429
1524
  # Exit with exit code
1430
- exit_code = 1 if len(warnings) > 0 else 0
1525
+ exit_code = 1 if len(errors) > 0 else 0
1431
1526
  if exit_code == 0:
1432
- console.print(f':tada: Task {name} tests passed ! You are free to make a PR.', style='bold green')
1527
+ console.print(f':tada: Task {name} tests passed !', style='bold green')
1433
1528
  else:
1434
- console.print(Error(message=f'Task {name} tests failed. Please fix the issues above before making a PR.'))
1529
+ console.print('\n[bold gold3]Errors:[/]')
1530
+ for error in errors:
1531
+ console.print(error)
1532
+ console.print(Error(message=f'Task {name} tests failed. Please fix the issues above.'))
1435
1533
 
1534
+ if warnings:
1535
+ console.print('\n[bold gold3]Warnings:[/]')
1536
+ for warning in warnings:
1537
+ console.print(warning)
1538
+
1539
+ console.print("\n")
1436
1540
  sys.exit(exit_code)
1437
1541
 
1438
1542
 
1439
- def check_error(condition, message, error, warnings=[]):
1543
+ def check_test(condition, message, fail_message, results=[], warn=False):
1440
1544
  console.print(f'[bold magenta]:zap: {message} ...[/]', end='')
1441
1545
  if not condition:
1442
- warning = Warning(message=error)
1443
- warnings.append(warning)
1444
- console.print(' [bold red]FAILED[/]', style='dim')
1546
+ if not warn:
1547
+ error = Error(message=fail_message)
1548
+ console.print(' [bold red]FAILED[/]', style='dim')
1549
+ results.append(error)
1550
+ else:
1551
+ warning = Warning(message=fail_message)
1552
+ console.print(' [bold yellow]WARNING[/]', style='dim')
1553
+ results.append(warning)
1445
1554
  else:
1446
1555
  console.print(' [bold green]OK[/]', style='dim')
1447
1556
  return True
@@ -7,10 +7,19 @@ input_types:
7
7
  - host
8
8
  - cidr_range
9
9
  tasks:
10
+ naabu:
11
+ description: Find open ports
12
+ ports: "-" # scan all ports
10
13
  nmap:
11
14
  description: Search for vulnerabilities on open ports
12
- skip_host_discovery: True
13
- ports: "-" # scan all ports
15
+ version_detection: True
16
+ script: vulners
17
+ targets_:
18
+ - port.host
19
+ - target.name
20
+ ports_:
21
+ - port.port
22
+ ports: "-" # default if no port found by naabu
14
23
  _group/1:
15
24
  httpx:
16
25
  description: Probe HTTP services on open ports
@@ -0,0 +1,39 @@
1
+ type: workflow
2
+ name: port_scan
3
+ alias: pscan
4
+ description: Port scan
5
+ tags: [recon, network, http, vuln]
6
+ input_types:
7
+ - host
8
+ - cidr_range
9
+ tasks:
10
+ naabu:
11
+ description: Find open ports
12
+ skip_host_discovery: True
13
+ ports: "-" # scan all ports
14
+ nmap:
15
+ description: Search for vulnerabilities on open ports
16
+ skip_host_discovery: True
17
+ version_detection: True
18
+ targets_: port.host
19
+ ports_: port.port
20
+ _group:
21
+ searchsploit:
22
+ description: Search for related exploits
23
+ targets_:
24
+ - type: port
25
+ field: '{host}~{service_name}'
26
+ condition: item._source.startswith('nmap') and len(item.service_name.split('/')) > 1
27
+ httpx:
28
+ description: Probe HTTP services on open ports
29
+ targets_:
30
+ - type: port
31
+ field: '{host}:{port}'
32
+ condition: item._source.startswith('nmap')
33
+ results:
34
+ - type: port
35
+
36
+ - type: url
37
+ condition: item.status_code != 0
38
+
39
+ - type: vulnerability
@@ -14,6 +14,11 @@ tasks:
14
14
  field: '{name}/FUZZ'
15
15
  cariddi:
16
16
  description: Crawl HTTP directories for content
17
+ info: True
18
+ secrets: True
19
+ errors: True
20
+ juicy_extensions: 1
21
+ juicy_endpoints: True
17
22
  targets_:
18
23
  - target.name
19
24
  - url.url
secator/definitions.py CHANGED
@@ -23,10 +23,6 @@ ASCII = rf"""
23
23
  DEBUG = CONFIG.debug.level
24
24
  DEBUG_COMPONENT = CONFIG.debug.component.split(',')
25
25
 
26
- # Default tasks settings
27
- DEFAULT_NUCLEI_FLAGS = os.environ.get('DEFAULT_NUCLEI_FLAGS', '-stats -sj -si 20 -hm -or')
28
- DEFAULT_FEROXBUSTER_FLAGS = os.environ.get('DEFAULT_FEROXBUSTER_FLAGS', '--auto-bail --no-state')
29
-
30
26
  # Constants
31
27
  OPT_NOT_SUPPORTED = -1
32
28
  OPT_PIPE_INPUT = -1
secator/installer.py CHANGED
@@ -31,8 +31,10 @@ class InstallerStatus(Enum):
31
31
  INSTALL_FAILED = 'INSTALL_FAILED'
32
32
  INSTALL_NOT_SUPPORTED = 'INSTALL_NOT_SUPPORTED'
33
33
  INSTALL_SKIPPED_OK = 'INSTALL_SKIPPED_OK'
34
+ INSTALL_VERSION_NOT_SPECIFIED = 'INSTALL_VERSION_NOT_SPECIFIED'
34
35
  GITHUB_LATEST_RELEASE_NOT_FOUND = 'GITHUB_LATEST_RELEASE_NOT_FOUND'
35
36
  GITHUB_RELEASE_NOT_FOUND = 'RELEASE_NOT_FOUND'
37
+ GITHUB_RELEASE_UNMATCHED_DISTRIBUTION = 'RELEASE_UNMATCHED_DISTRIBUTION'
36
38
  GITHUB_RELEASE_FAILED_DOWNLOAD = 'GITHUB_RELEASE_FAILED_DOWNLOAD'
37
39
  GITHUB_BINARY_NOT_FOUND_IN_ARCHIVE = 'GITHUB_BINARY_NOT_FOUND_IN_ARCHIVE'
38
40
  UNKNOWN_DISTRIBUTION = 'UNKNOWN_DISTRIBUTION'
@@ -83,12 +85,12 @@ class ToolInstaller:
83
85
  # Install binaries from GH
84
86
  gh_status = InstallerStatus.UNKNOWN
85
87
  if tool_cls.install_github_handle and not CONFIG.security.force_source_install:
86
- gh_status = GithubInstaller.install(tool_cls.install_github_handle)
88
+ gh_status = GithubInstaller.install(tool_cls.install_github_handle, version=tool_cls.install_version or 'latest')
87
89
  status = gh_status
88
90
 
89
91
  # Install from source
90
92
  if tool_cls.install_cmd and not gh_status.is_ok():
91
- status = SourceInstaller.install(tool_cls.install_cmd)
93
+ status = SourceInstaller.install(tool_cls.install_cmd, tool_cls.install_version)
92
94
  if not status.is_ok():
93
95
  cls.print_status(status, name)
94
96
  return status
@@ -167,12 +169,14 @@ class SourceInstaller:
167
169
  """Install a tool from source."""
168
170
 
169
171
  @classmethod
170
- def install(cls, config, install_prereqs=True):
172
+ def install(cls, config, version=None, install_prereqs=True):
171
173
  """Install from source.
172
174
 
173
175
  Args:
174
176
  cls: ToolInstaller class.
175
177
  config (dict): A dict of distros as keys and a command as value.
178
+ version (str, optional): Version to install.
179
+ install_prereqs (bool, optional): Install pre-requisites.
176
180
 
177
181
  Returns:
178
182
  Status: install status.
@@ -204,6 +208,11 @@ class SourceInstaller:
204
208
  if not status.is_ok():
205
209
  return status
206
210
 
211
+ # Handle version
212
+ if '[install_version]' in install_cmd:
213
+ version = version or 'latest'
214
+ install_cmd = install_cmd.replace('[install_version]', version)
215
+
207
216
  # Run command
208
217
  ret = Command.execute(install_cmd, cls_attributes={'shell': True}, quiet=False)
209
218
  return InstallerStatus.SUCCESS if ret.return_code == 0 else InstallerStatus.INSTALL_FAILED
@@ -213,7 +222,7 @@ class GithubInstaller:
213
222
  """Install a tool from GitHub releases."""
214
223
 
215
224
  @classmethod
216
- def install(cls, github_handle):
225
+ def install(cls, github_handle, version='latest'):
217
226
  """Find and install a release from a GitHub handle {user}/{repo}.
218
227
 
219
228
  Args:
@@ -223,35 +232,38 @@ class GithubInstaller:
223
232
  InstallerStatus: status.
224
233
  """
225
234
  _, repo = tuple(github_handle.split('/'))
226
- latest_release = cls.get_latest_release(github_handle)
227
- if not latest_release:
228
- return InstallerStatus.GITHUB_LATEST_RELEASE_NOT_FOUND
235
+ release = cls.get_release(github_handle, version=version)
236
+ if not release:
237
+ return InstallerStatus.GITHUB_RELEASE_NOT_FOUND
229
238
 
230
239
  # Find the right asset to download
231
240
  system, arch, os_identifiers, arch_identifiers = cls._get_platform_identifier()
232
- download_url = cls._find_matching_asset(latest_release['assets'], os_identifiers, arch_identifiers)
241
+ download_url = cls._find_matching_asset(release['assets'], os_identifiers, arch_identifiers)
233
242
  if not download_url:
234
243
  console.print(Error(message=f'Could not find a GitHub release matching distribution (system: {system}, arch: {arch}).')) # noqa: E501
235
- return InstallerStatus.GITHUB_RELEASE_NOT_FOUND
244
+ return InstallerStatus.GITHUB_RELEASE_UNMATCHED_DISTRIBUTION
236
245
 
237
246
  # Download and unpack asset
238
247
  console.print(Info(message=f'Found release URL: {download_url}'))
239
248
  return cls._download_and_unpack(download_url, CONFIG.dirs.bin, repo)
240
249
 
241
250
  @classmethod
242
- def get_latest_release(cls, github_handle):
243
- """Get latest release from GitHub.
251
+ def get_release(cls, github_handle, version='latest'):
252
+ """Get release from GitHub.
244
253
 
245
254
  Args:
246
255
  github_handle (str): A GitHub handle {user}/{repo}.
247
256
 
248
257
  Returns:
249
- dict: Latest release JSON from GitHub releases.
258
+ dict: Release JSON from GitHub releases.
250
259
  """
251
260
  if not github_handle:
252
261
  return False
253
262
  owner, repo = tuple(github_handle.split('/'))
254
- url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
263
+ if version == 'latest':
264
+ url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
265
+ else:
266
+ url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{version}"
255
267
  headers = {}
256
268
  if CONFIG.cli.github_token:
257
269
  headers['Authorization'] = f'Bearer {CONFIG.cli.github_token}'
@@ -268,7 +280,7 @@ class GithubInstaller:
268
280
 
269
281
  @classmethod
270
282
  def get_latest_version(cls, github_handle):
271
- latest_release = cls.get_latest_release(github_handle)
283
+ latest_release = cls.get_release(github_handle, version='latest')
272
284
  if not latest_release:
273
285
  return None
274
286
  return latest_release['tag_name'].lstrip('v')
@@ -444,6 +456,7 @@ def get_version_info(name, version_flag=None, install_github_handle=None, instal
444
456
  'latest_version': None,
445
457
  'location': None,
446
458
  'status': '',
459
+ 'outdated': False,
447
460
  'errors': []
448
461
  }
449
462
 
@@ -504,6 +517,7 @@ def get_version_info(name, version_flag=None, install_github_handle=None, instal
504
517
  if version and latest_version:
505
518
  if parse_version(version) < parse_version(latest_version):
506
519
  info['status'] = 'outdated'
520
+ info['outdated'] = True
507
521
  else:
508
522
  info['status'] = 'latest'
509
523
  elif not version:
@@ -529,7 +543,7 @@ def get_distro_config():
529
543
  distrib = system
530
544
 
531
545
  if system == "Linux":
532
- distrib = distro.id()
546
+ distrib = distro.id() or distro.like()
533
547
 
534
548
  if distrib in ["ubuntu", "debian", "linuxmint", "popos", "kali"]:
535
549
  installer = "apt install -y --no-install-recommends"
secator/runners/_base.py CHANGED
@@ -130,7 +130,8 @@ class Runner:
130
130
 
131
131
  # Determine inputs
132
132
  self.inputs = [inputs] if not isinstance(inputs, list) else inputs
133
- self.filter_results(results)
133
+ targets = [Target(name=target) for target in self.inputs]
134
+ self.filter_results(results + targets)
134
135
 
135
136
  # Debug
136
137
  self.debug('Inputs', obj=self.inputs, sub='init')
@@ -18,14 +18,25 @@ def run_extractors(results, opts, inputs=[], dry_run=False):
18
18
  """
19
19
  extractors = {k: v for k, v in opts.items() if k.endswith('_')}
20
20
  errors = []
21
+ computed_inputs = []
22
+ computed_opts = {}
21
23
  for key, val in extractors.items():
22
24
  key = key.rstrip('_')
23
25
  values, err = extract_from_results(results, val)
24
26
  errors.extend(err)
25
27
  if key == 'targets':
26
- inputs = ['<COMPUTED>'] if dry_run else deduplicate(values)
28
+ targets = ['<COMPUTED>'] if dry_run else deduplicate(values)
29
+ computed_inputs.extend(targets)
27
30
  else:
28
- opts[key] = ['<COMPUTED>'] if dry_run else deduplicate(values)
31
+ computed_opt = ['<COMPUTED>'] if dry_run else deduplicate(values)
32
+ if computed_opt:
33
+ computed_opts[key] = computed_opt
34
+ opts[key] = computed_opts[key]
35
+ if computed_inputs:
36
+ debug('computed_inputs', obj=computed_inputs, sub='extractors')
37
+ inputs = computed_inputs
38
+ if computed_opts:
39
+ debug('computed_opts', obj=computed_opts, sub='extractors')
29
40
  return inputs, opts, errors
30
41
 
31
42
 
@@ -84,6 +84,7 @@ class Command(Runner):
84
84
  install_post = None
85
85
  install_cmd = None
86
86
  install_github_handle = None
87
+ install_version = None
87
88
 
88
89
  # Serializer
89
90
  item_loader = None
@@ -246,7 +247,7 @@ class Command(Runner):
246
247
  def get_version_info(cls):
247
248
  from secator.installer import get_version_info
248
249
  return get_version_info(
249
- cls.__name__,
250
+ cls.cmd.split(' ')[0],
250
251
  cls.get_version_flag(),
251
252
  cls.install_github_handle,
252
253
  cls.install_cmd
@@ -27,7 +27,7 @@ USER_AGENTS = {
27
27
  OPTS = {
28
28
  HEADER: {'type': str, 'help': 'Custom header to add to each request in the form "KEY1:VALUE1; KEY2:VALUE2"', 'default': 'User-Agent: ' + USER_AGENTS['chrome_134.0_win10']}, # noqa: E501
29
29
  DELAY: {'type': float, 'short': 'd', 'help': 'Delay to add between each requests'},
30
- DEPTH: {'type': int, 'help': 'Scan depth', 'default': 2},
30
+ DEPTH: {'type': int, 'help': 'Scan depth'},
31
31
  FILTER_CODES: {'type': str, 'short': 'fc', 'help': 'Filter out responses with HTTP codes'},
32
32
  FILTER_REGEX: {'type': str, 'short': 'fr', 'help': 'Filter out responses with regular expression'},
33
33
  FILTER_SIZE: {'type': str, 'short': 'fs', 'help': 'Filter out responses with size'},
@@ -41,7 +41,7 @@ OPTS = {
41
41
  PROXY: {'type': str, 'help': 'HTTP(s) / SOCKS5 proxy'},
42
42
  RATE_LIMIT: {'type': int, 'short': 'rl', 'help': 'Rate limit, i.e max number of requests per second'},
43
43
  RETRIES: {'type': int, 'help': 'Retries'},
44
- THREADS: {'type': int, 'help': 'Number of threads to run', 'default': 50},
44
+ THREADS: {'type': int, 'help': 'Number of threads to run'},
45
45
  TIMEOUT: {'type': int, 'help': 'Request timeout'},
46
46
  USER_AGENT: {'type': str, 'short': 'ua', 'help': 'User agent, e.g "Mozilla Firefox 1.0"'},
47
47
  WORDLIST: {'type': str, 'short': 'w', 'default': 'http', 'process': process_wordlist, 'help': 'Wordlist to use'}
secator/tasks/arjun.py CHANGED
@@ -49,7 +49,8 @@ class arjun(Command):
49
49
  'follow_redirect': '--follow-redirect',
50
50
  }
51
51
  output_types = [Url]
52
- install_cmd = 'pipx install arjun && pipx upgrade arjun'
52
+ install_version = '2.2.7'
53
+ install_cmd = 'pipx install arjun==[install_version] --force'
53
54
  install_github_handle = 's0md3v/Arjun'
54
55
 
55
56
  @staticmethod