diff --git a/docs/docs/development/api.md b/docs/docs/development/api.md new file mode 100644 index 00000000..37062eac --- /dev/null +++ b/docs/docs/development/api.md @@ -0,0 +1,22 @@ +# API + +Every frontend-available data and more is accessible through the API. Simply create your API key in your profile, and then add it as a header when querying the API. + +The API is available at `/api/`. You can do an unauthenticated request at `/api/ping` that replies a simple string, and an authenticated request at `/api/` that gets the version. + +Unauthenticated `GET` request to `/api/ping`: + +```bash +curl https://your-red-kite-url/api/ping +``` + +# API Key + +Generate your API key in your profile page, giving it a meaningful name and an expiration date. Then, use it as a header in your following requests. + +Authenticated `GET` request to `/api/`: + +```bash +export MY_KEY="my key value" +curl -H "x-api-key: $MY_KEY" https://your-red-kite-url/api/ +``` diff --git a/packages/backend/jobs-manager/service/src/modules/database/custom-jobs/custom-jobs.controller.ts b/packages/backend/jobs-manager/service/src/modules/database/custom-jobs/custom-jobs.controller.ts index 1cecd55d..9b4d0a97 100644 --- a/packages/backend/jobs-manager/service/src/modules/database/custom-jobs/custom-jobs.controller.ts +++ b/packages/backend/jobs-manager/service/src/modules/database/custom-jobs/custom-jobs.controller.ts @@ -45,7 +45,6 @@ export class CustomJobsController { return await this.customJobsService.duplicate(dto.jobId); } else { const jobDto = plainToInstance(JobDto, dto); - console.log(jobDto); await validateOrReject(jobDto); return await this.customJobsService.create(dto); } diff --git a/packages/backend/jobs-manager/service/src/modules/database/reporting/findings/finding.model.ts b/packages/backend/jobs-manager/service/src/modules/database/reporting/findings/finding.model.ts index 5f96e03d..2914041e 100644 --- a/packages/backend/jobs-manager/service/src/modules/database/reporting/findings/finding.model.ts +++ b/packages/backend/jobs-manager/service/src/modules/database/reporting/findings/finding.model.ts @@ -50,7 +50,7 @@ export class CustomFinding { @Prop() public name: string; - @Prop() + @Prop({ index: true }) public key: string; @Prop({ index: true }) diff --git a/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.dto.ts b/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.dto.ts index 27d8c5fd..a13e8190 100644 --- a/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.dto.ts +++ b/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.dto.ts @@ -7,6 +7,7 @@ import { IsMongoId, IsNotEmpty, IsOptional, + IsString, } from 'class-validator'; import { Types } from 'mongoose'; import { PortDetailsLevel, portDetailsLevel } from '../../database.constants'; @@ -33,6 +34,21 @@ export class PortFilterDto extends IntersectionType( @IsOptional() @IsMongoId() hostId: string; + + @IsOptional() + @IsString({ each: true }) + @IsArray() + services: string[]; + + @IsOptional() + @IsString({ each: true }) + @IsArray() + products: string[]; + + @IsOptional() + @IsString({ each: true }) + @IsArray() + versions: string[]; } export class GetPortsDto extends IntersectionType(PagingDto, PortFilterDto) {} diff --git a/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.model.ts b/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.model.ts index a17a0bef..58503e3e 100644 --- a/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.model.ts +++ b/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.model.ts @@ -56,6 +56,12 @@ export class Port { @Prop() service: string; + + @Prop() + product: string; + + @Prop() + version: string; } export const PortSchema = SchemaFactory.createForClass(Port); diff --git a/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.service.spec.ts b/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.service.spec.ts index c39a3a30..e638788e 100644 --- a/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.service.spec.ts +++ b/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.service.spec.ts @@ -1,4 +1,6 @@ +import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; +import { Model } from 'mongoose'; import { getName } from '../../../../test/test.utils'; import { AppModule } from '../../../app.module'; import { TagsDocument } from '../../tags/tag.model'; @@ -8,6 +10,7 @@ import { HostService } from '../host/host.service'; import { CreateProjectDto } from '../project.dto'; import { ProjectService } from '../project.service'; import { GetPortsDto } from './port.dto'; +import { Port, PortDocument } from './port.model'; import { PortService } from './port.service'; describe('Port Service', () => { @@ -17,6 +20,7 @@ describe('Port Service', () => { let projectService: ProjectService; let tagsService: TagsService; let portService: PortService; + let portModel: Model; const testPrefix = 'port-service-ut'; beforeAll(async () => { @@ -28,6 +32,7 @@ describe('Port Service', () => { projectService = moduleFixture.get(ProjectService); tagsService = moduleFixture.get(TagsService); portService = moduleFixture.get(PortService); + portModel = moduleFixture.get>(getModelToken('port')); }); beforeEach(async () => { @@ -566,6 +571,89 @@ describe('Port Service', () => { ); }, ); + + it.each([ + { + service: 'asdf', + product: 'qwerty', + version: 'uiop', + dto: { services: ['asdf1', 'asdf2'] }, + expectedPorts: [1, 2], + }, + { + service: 'asdf', + product: 'qwerty', + version: 'uiop', + dto: { services: ['asdf1', 'asdf2'], products: ['qwerty1'] }, + expectedPorts: [1], + }, + { + service: 'asdf', + product: 'qwerty', + version: 'uiop', + dto: { services: ['asdf1', 'asdf2'], versions: ['uiop2'] }, + expectedPorts: [2], + }, + { + service: 'asdf', + product: 'qWErty', + version: 'uiop', + dto: { services: ['asdf1', 'asdf2'], products: ['qwerty2', 'qwerty3'] }, + expectedPorts: [2], + }, + { + service: 'asdf', + product: 'qWErty', + version: 'uiOP', + dto: { versions: ['uiop3'], products: ['qwerty2', 'qwerty3'] }, + expectedPorts: [3], + }, + ])( + 'Filter by service, product, version: %s', + async ({ service, product, version, dto, expectedPorts }) => { + // Arrange + const p1 = await project('p1'); + + const hosts = [{ host: '1.1.1.1', ports: [1, 2, 3, 4] }]; + const hDocs = []; + + const ports: PortDocument[] = []; + + for (const h of hosts) { + const htmp = await host(h.host, p1._id.toString()); + hDocs.push(htmp[0]); + for (const p of h.ports) { + ports.push( + await portService.addPort( + htmp[0]._id.toString(), + p1._id.toString(), + p, + 'tcp', + ), + ); + } + } + + for (const port of ports) { + await portModel.updateOne( + { _id: { $eq: port._id } }, + { + service: service + port.port, + product: product + port.port, + version: version + port.port, + }, + ); + } + + // Act + const allPorts = await portService.getAll(0, 10, dto); + + // Assert + expect(allPorts.map((x) => x.port).sort()).toStrictEqual( + expectedPorts.sort(), + ); + }, + ); }); async function project(name: string = '') { diff --git a/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.service.ts b/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.service.ts index 7089e306..0bab0010 100644 --- a/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.service.ts +++ b/packages/backend/jobs-manager/service/src/modules/database/reporting/port/port.service.ts @@ -42,6 +42,8 @@ export class PortService { portNumber: number, protocol: 'tcp' | 'udp', service: string = undefined, + product: string = undefined, + version: string = undefined, ) { const host: Pick = await this.hostModel.findOne( { @@ -64,6 +66,8 @@ export class PortService { protocol, correlationKey, service, + product, + version, ); } @@ -111,6 +115,8 @@ export class PortService { protocol: string, correlationKey: string, service: string = undefined, + product: string = undefined, + version: string = undefined, ) { if (!(protocol === 'tcp' || protocol === 'udp')) throw new HttpBadRequestException(this.badProtocolError); @@ -143,7 +149,12 @@ export class PortService { let setter = service === undefined ? { lastSeen: Date.now() } - : { lastSeen: Date.now(), service: service }; + : { + lastSeen: Date.now(), + service: service, + product: product, + version: version, + }; res = await this.portsModel.findOneAndUpdate( { @@ -385,6 +396,45 @@ export class PortService { } } + // Filter by port service + if (dto.services) { + const servicesRegex = dto.services + .filter((x) => x) + .map((x) => x.toLowerCase().trim()) + .map((x) => escapeStringRegexp(x)) + .map((x) => new RegExp(`.*${x}.*`)); + + if (servicesRegex.length > 0) { + finalFilter['service'] = { $in: servicesRegex }; + } + } + + // Filter by port products + if (dto.products) { + const productsRegex = dto.products + .filter((x) => x) + .map((x) => x.toLowerCase().trim()) + .map((x) => escapeStringRegexp(x)) + .map((x) => new RegExp(`.*${x}.*`, 'i')); + + if (productsRegex.length > 0) { + finalFilter['product'] = { $in: productsRegex }; + } + } + + // Filter by port versions + if (dto.versions) { + const versionsRegex = dto.versions + .filter((x) => x) + .map((x) => x.toLowerCase().trim()) + .map((x) => escapeStringRegexp(x)) + .map((x) => new RegExp(`.*${x}.*`, 'i')); + + if (versionsRegex.length > 0) { + finalFilter['version'] = { $in: versionsRegex }; + } + } + // Filter by project if (dto.projects) { const projectIds = dto.projects diff --git a/packages/backend/jobs-manager/service/src/modules/findings/commands/JobFindings/custom.handler.ts b/packages/backend/jobs-manager/service/src/modules/findings/commands/JobFindings/custom.handler.ts index 2b80935c..b79d78ea 100644 --- a/packages/backend/jobs-manager/service/src/modules/findings/commands/JobFindings/custom.handler.ts +++ b/packages/backend/jobs-manager/service/src/modules/findings/commands/JobFindings/custom.handler.ts @@ -42,9 +42,15 @@ export class CustomFindingHandler extends JobFindingHandlerBase { const service = CorrelationKeyUtils.getResourceServiceName( command.finding.correlationKey, ); - console.log(service); - console.log(command.finding.correlationKey); switch (service) { case 'DomainsService': diff --git a/packages/backend/jobs-manager/service/src/modules/findings/finding.dto.ts b/packages/backend/jobs-manager/service/src/modules/findings/finding.dto.ts index 05fc6e0c..82457177 100644 --- a/packages/backend/jobs-manager/service/src/modules/findings/finding.dto.ts +++ b/packages/backend/jobs-manager/service/src/modules/findings/finding.dto.ts @@ -2,6 +2,7 @@ import { IntersectionType } from '@nestjs/swagger'; import { Transform, Type, plainToClass } from 'class-transformer'; import { IsArray, + IsBoolean, IsNotEmpty, IsOptional, IsString, @@ -9,7 +10,9 @@ import { isArray, } from 'class-validator'; import { HttpBadRequestException } from '../../exceptions/http.exceptions'; +import { booleanStringToBoolean } from '../../utils/boolean-string-to-boolean'; import { PagingDto } from '../database/database.dto'; +import { FilterByProjectDto } from '../database/reporting/resource.dto'; export type CustomFindingFieldDto = CustomFindingBaseDto & (CustomFindingImageFieldDto | CustomFindingTextFieldDto); @@ -50,20 +53,20 @@ export class FieldFilterDto { data: unknown; } -export class FindingsFilterDto { +export class FindingsFilterDto extends FilterByProjectDto { @IsString({ each: true }) @IsArray() - targets: string[]; + targets?: string[]; @IsString({ each: true }) @IsArray() @IsOptional() - findingDenyList: string[]; + findingDenyList?: string[]; @IsString({ each: true }) @IsArray() @IsOptional() - findingAllowList: string[]; + findingAllowList?: string[]; @IsArray() @ValidateNested({ each: true }) @@ -86,7 +89,12 @@ export class FindingsFilterDto { { toClassOnly: true }, ) @IsOptional() - fieldFilters: FieldFilterDto[]; + fieldFilters?: FieldFilterDto[]; + + @IsBoolean() + @Transform(booleanStringToBoolean) + @IsOptional() + latestOnly?: boolean; } export class FindingsPagingDto extends IntersectionType( diff --git a/packages/backend/jobs-manager/service/src/modules/findings/findings-filter.type.ts b/packages/backend/jobs-manager/service/src/modules/findings/findings-filter.type.ts deleted file mode 100644 index 16e07b22..00000000 --- a/packages/backend/jobs-manager/service/src/modules/findings/findings-filter.type.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface FieldFilter { - key: string; - data?: unknown; -} - -export interface FindingsFilter { - targets?: string[]; - findingDenyList?: string[]; - findingAllowList?: string[]; - fieldFilters?: FieldFilter[]; -} diff --git a/packages/backend/jobs-manager/service/src/modules/findings/findings.constants.ts b/packages/backend/jobs-manager/service/src/modules/findings/findings.constants.ts index accad2d5..db0483f6 100644 --- a/packages/backend/jobs-manager/service/src/modules/findings/findings.constants.ts +++ b/packages/backend/jobs-manager/service/src/modules/findings/findings.constants.ts @@ -1,6 +1,8 @@ export class CustomFindingsConstants { public static readonly PortServiceFinding = 'PortServiceFinding'; public static readonly ServiceNameFieldKey = 'serviceName'; + public static readonly ServiceProductFieldKey = 'serviceProduct'; + public static readonly ServiceVersionFieldKey = 'serviceVersion'; public static readonly WebsitePathFinding = 'WebsitePathFinding'; public static readonly WebsiteEndpointFieldKey = 'endpoint'; diff --git a/packages/backend/jobs-manager/service/src/modules/findings/findings.service.spec.ts b/packages/backend/jobs-manager/service/src/modules/findings/findings.service.spec.ts index 1d9aba6c..08dd1e13 100644 --- a/packages/backend/jobs-manager/service/src/modules/findings/findings.service.spec.ts +++ b/packages/backend/jobs-manager/service/src/modules/findings/findings.service.spec.ts @@ -137,6 +137,135 @@ describe('Findings Service Spec', () => { expect(firstPage.totalRecords).toBe(4); }); + it('Get all - Should only return from the findings from project', async () => { + // Arrange + const p = await project(); + const p2 = await project(); + const p3 = await project(); + + const correlationKey = CorrelationKeyUtils.generateCorrelationKey( + p._id.toString(), + ); + const correlationKey2 = CorrelationKeyUtils.generateCorrelationKey( + p2._id.toString(), + ); + const correlationKey3 = CorrelationKeyUtils.generateCorrelationKey( + p3._id.toString(), + ); + + await findingsModel.insertMany([ + { + created: new Date(2011, 1, 1), + correlationKey: correlationKey, + name: 'b', + }, + { + created: new Date(2010, 1, 1), + correlationKey: correlationKey, + name: 'a', + }, + { + created: new Date(2022, 1, 1), + correlationKey: correlationKey2, + name: 'should not be returned', + }, + { + created: new Date(2013, 1, 1), + correlationKey: correlationKey3, + name: 'should not be returned', + }, + ]); + + // Act + const firstPage = await findingsService.getAll(0, 5, { + projects: [p2._id.toString(), p3._id.toString()], + }); + + // Assert + expect(firstPage.totalRecords).toBe(2); + expect(firstPage.items.map((f) => f.correlationKey).sort()).toStrictEqual( + [`project:${p2._id.toString()}`, `project:${p3._id.toString()}`].sort(), + ); + }); + + it.each([ + { + page: 0, + pageSize: 5, + filter: { latestOnly: true }, + expectedResult: ['a', 'c'], + }, + { + page: 0, + pageSize: 1, + filter: { latestOnly: true }, + expectedResult: ['c'], + }, + { + page: 1, + pageSize: 1, + filter: { latestOnly: true }, + expectedResult: ['a'], + }, + { + page: 0, + pageSize: 5, + filter: { latestOnly: true, findingAllowList: ['qwerty'] }, + expectedResult: ['c'], + }, + { + page: 0, + pageSize: 5, + filter: { latestOnly: true, findingAllowList: ['returnnothing'] }, + expectedResult: [], + }, + ])( + 'Get all - Should only return from the latest of every finding (latestOnly: true): %s', + async ({ page, pageSize, filter, expectedResult }) => { + // Arrange + const p = await project(); + + const correlationKey = CorrelationKeyUtils.generateCorrelationKey( + p._id.toString(), + ); + + await findingsModel.insertMany([ + { + created: new Date(2011, 1, 1), + key: 'asdf', + correlationKey: correlationKey, + name: 'a', + }, + { + created: new Date(2010, 1, 1), + key: 'asdf', + correlationKey: correlationKey, + name: 'b', + }, + { + created: new Date(2022, 1, 1), + key: 'qwerty', + correlationKey: correlationKey, + name: 'c', + }, + { + created: new Date(2013, 1, 1), + key: 'qwerty', + correlationKey: correlationKey, + name: 'd', + }, + ]); + + // Act + const firstPage = await findingsService.getAll(page, pageSize, filter); + + // Assert + expect(firstPage.items.map((f) => f.name).sort()).toStrictEqual( + expectedResult.sort(), + ); + }, + ); + it('Save - Nonexistent project - Throws', async () => { // Arrange // Act diff --git a/packages/backend/jobs-manager/service/src/modules/findings/findings.service.ts b/packages/backend/jobs-manager/service/src/modules/findings/findings.service.ts index 40442d50..bd1d2a56 100644 --- a/packages/backend/jobs-manager/service/src/modules/findings/findings.service.ts +++ b/packages/backend/jobs-manager/service/src/modules/findings/findings.service.ts @@ -22,8 +22,7 @@ import { HostnameIpCommand } from './commands/JobFindings/hostname-ip.command'; import { PortCommand } from './commands/JobFindings/port.command'; import { TagCommand } from './commands/JobFindings/tag.command'; import { WebsiteCommand } from './commands/JobFindings/website.command'; -import { CustomFindingFieldDto } from './finding.dto'; -import { FindingsFilter } from './findings-filter.type'; +import { CustomFindingFieldDto, FindingsFilterDto } from './finding.dto'; export type Finding = | HostnameIpFinding @@ -244,30 +243,119 @@ export class FindingsService { public async getAll( page: number, pageSize: number, - dto: FindingsFilter = undefined, + dto: FindingsFilterDto = undefined, ): Promise> { if (page < 0) throw new HttpBadRequestException('Page starts at 0.'); - const filters: FilterQuery = this.buildFilters(dto); - - const items = await this.findingModel - .find(filters) - .sort({ - created: 'desc', - }) - .skip(page * pageSize) - .limit(pageSize) - .exec(); - const totalRecords = await this.findingModel.countDocuments(filters); - - return { - items, - totalRecords, - }; + return await this.getAllFindings( + this.buildFilters(dto), + !!dto.latestOnly, + page, + pageSize, + ); } - private buildFilters(dto: FindingsFilter): FilterQuery { + private async getAllFindings( + filters: FilterQuery, + latestOnly: boolean, + page: number, + pageSize: number, + ): Promise> { + if (!latestOnly) { + const items = await this.findingModel + .find(filters) + .sort({ + created: 'desc', + }) + .skip(page * pageSize) + .limit(pageSize) + .exec(); + const totalRecords = await this.findingModel.countDocuments(filters); + return { + items, + totalRecords, + }; + } + + return ( + // This query returns only the latest finding by key for every resource, + // in the page format with the count + ( + await this.findingModel.aggregate>( + [ + { $sort: { created: -1 } }, + { + $match: filters, + }, + { + $group: { + _id: { + key: '$key', + correlationKey: '$correlationKey', + }, + name: { $first: '$name' }, + fields: { $first: '$fields' }, + id: { $first: '$_id' }, + created: { $first: '$created' }, + }, + }, + { + $group: { + _id: '$_id.key', + data: { + $addToSet: { + _id: '$id', + key: '$_id.key', + correlationKey: '$_id.correlationKey', + name: '$name', + fields: '$fields', + created: '$created', + }, + }, + }, + }, + { + $unwind: { + path: '$data', + preserveNullAndEmptyArrays: false, + }, + }, + { + $facet: { + counting: [{ $count: 'count' }], + items: [ + { $sort: { 'data.created': -1 } }, + { $skip: page * pageSize }, + { $limit: pageSize }, + { + $project: { + _id: '$data._id', + key: '$data.key', + correlationKey: '$data.correlationKey', + name: '$data.name', + fields: '$data.fields', + created: '$data.created', + }, + }, + ], + }, + }, + { + $project: { + totalRecords: { $first: '$counting.count' }, + items: '$items', + }, + }, + ], + { maxTimeMS: 60000, allowDiskUse: true }, + ) + )[0] + ); + } + + private buildFilters(dto: FindingsFilterDto): FilterQuery { const filters: FilterQuery = {}; + filters.$and = []; if (dto.targets && dto.targets.length > 0) { if (dto.targets.length === 1) { @@ -286,8 +374,6 @@ export class FindingsService { dto.findingAllowList?.length || dto.fieldFilters?.length ) { - filters.$and = []; - if (dto.findingDenyList?.length) { filters.$and.push({ key: { $nin: dto.findingDenyList } }); } @@ -311,6 +397,19 @@ export class FindingsService { } } + if (dto.projects) { + const correlationKeys = []; + for (const id of dto.projects) { + const correlationKey: string = + CorrelationKeyUtils.generateCorrelationKey(id); + correlationKeys.push(new RegExp(`${correlationKey}.*`)); + } + + filters.$and.push({ correlationKey: { $in: correlationKeys } }); + } + + if (!filters.$and.length) delete filters.$and; + return filters; } diff --git a/packages/frontend/stalker-app/src/app/api/jobs/jobs/jobs.service.ts b/packages/frontend/stalker-app/src/app/api/jobs/jobs/jobs.service.ts index 9c66893e..20bca660 100644 --- a/packages/frontend/stalker-app/src/app/api/jobs/jobs/jobs.service.ts +++ b/packages/frontend/stalker-app/src/app/api/jobs/jobs/jobs.service.ts @@ -59,6 +59,6 @@ export class JobsService { private filterJob(job: CustomJob, filters: string[]) { const parts = [job.name, job.findingHandlerLanguage, job.type]; - return filters.some((filter) => normalizeSearchString(parts.join(' ')).includes(filter)); + return filters.some((filter) => normalizeSearchString(parts.join(' ')).includes(normalizeSearchString(filter))); } } diff --git a/packages/frontend/stalker-app/src/app/api/jobs/subscriptions/subscriptions.service.ts b/packages/frontend/stalker-app/src/app/api/jobs/subscriptions/subscriptions.service.ts index 830a7c71..bb044677 100644 --- a/packages/frontend/stalker-app/src/app/api/jobs/subscriptions/subscriptions.service.ts +++ b/packages/frontend/stalker-app/src/app/api/jobs/subscriptions/subscriptions.service.ts @@ -1,13 +1,13 @@ import { Injectable } from '@angular/core'; import { Observable, combineLatest, map } from 'rxjs'; +import { stringify } from 'yaml'; +import { Page } from '../../../shared/types/page.type'; import { CronSubscription, CronSubscriptionData, EventSubscription, EventSubscriptionData, } from '../../../shared/types/subscriptions/subscription.type'; -import { stringify } from 'yaml'; -import { Page } from '../../../shared/types/page.type'; import { normalizeSearchString } from '../../../utils/normalize-search-string'; import { CronSubscriptionsService } from './cron-subscriptions.service'; import { EventSubscriptionsService } from './event-subscriptions.service'; @@ -116,6 +116,6 @@ export class SubscriptionService { cron.cronExpression ? 'cron' : 'event', subscription.isEnabled === false ? 'disabled' : 'enabled', ]; - return filters.some((filter) => normalizeSearchString(parts.join(' ')).includes(filter)); + return filters.some((filter) => normalizeSearchString(parts.join(' ')).includes(normalizeSearchString(filter))); } } diff --git a/packages/frontend/stalker-app/src/app/api/tags/tags.service.ts b/packages/frontend/stalker-app/src/app/api/tags/tags.service.ts index c4af8c31..efc88b50 100644 --- a/packages/frontend/stalker-app/src/app/api/tags/tags.service.ts +++ b/packages/frontend/stalker-app/src/app/api/tags/tags.service.ts @@ -36,6 +36,6 @@ export class TagsService { } private filterTags(secret: Tag, filters: string[]) { const parts = [secret?.text, secret.color]; - return filters.some((filter) => normalizeSearchString(parts.join(' ')).includes(filter)); + return filters.some((filter) => normalizeSearchString(parts.join(' ')).includes(normalizeSearchString(filter))); } } diff --git a/packages/frontend/stalker-app/src/app/modules/findings/finding/finding.component.ts b/packages/frontend/stalker-app/src/app/modules/findings/finding/finding.component.ts index b3ceebd2..690f7a7d 100644 --- a/packages/frontend/stalker-app/src/app/modules/findings/finding/finding.component.ts +++ b/packages/frontend/stalker-app/src/app/modules/findings/finding/finding.component.ts @@ -13,7 +13,7 @@ export class FindingComponent { constructor(private toastr: ToastrService) {} public copyJsonToClipboard() { - navigator.clipboard.writeText(JSON.stringify(this.finding)); + navigator.clipboard.writeText(JSON.stringify(this.finding, undefined, 2)); this.toastr.success( $localize`:Finding copied to clipboard|Finding copied to clipboard:Finding copied to clipboard` ); diff --git a/packages/frontend/stalker-app/src/app/modules/ip-ranges/list-ip-ranges/list-ip-ranges.component.ts b/packages/frontend/stalker-app/src/app/modules/ip-ranges/list-ip-ranges/list-ip-ranges.component.ts index 5147a512..b75d5ee4 100644 --- a/packages/frontend/stalker-app/src/app/modules/ip-ranges/list-ip-ranges/list-ip-ranges.component.ts +++ b/packages/frontend/stalker-app/src/app/modules/ip-ranges/list-ip-ranges/list-ip-ranges.component.ts @@ -269,7 +269,6 @@ export class ListIpRangesComponent { } dateFilter(event: MouseEvent) { - console.log('DATE FILTER'); event.stopPropagation(); this.startDate = new Date(Date.now() - defaultNewTimeMs); } diff --git a/packages/frontend/stalker-app/src/app/modules/jobs/launch-jobs/launch-jobs.component.ts b/packages/frontend/stalker-app/src/app/modules/jobs/launch-jobs/launch-jobs.component.ts index 9497b6e1..94871f08 100644 --- a/packages/frontend/stalker-app/src/app/modules/jobs/launch-jobs/launch-jobs.component.ts +++ b/packages/frontend/stalker-app/src/app/modules/jobs/launch-jobs/launch-jobs.component.ts @@ -240,6 +240,6 @@ export class LaunchJobsComponent implements AfterViewInit { private filterJob(entry: JobListEntry, filter: string) { const parts = [entry.name]; - return normalizeSearchString(parts.join(' ')).includes(filter); + return normalizeSearchString(parts.join(' ')).includes(normalizeSearchString(filter)); } } diff --git a/packages/frontend/stalker-app/src/app/modules/jobs/subscriptions/list-subscriptions.component.ts b/packages/frontend/stalker-app/src/app/modules/jobs/subscriptions/list-subscriptions.component.ts index 7aba989f..11c90b64 100644 --- a/packages/frontend/stalker-app/src/app/modules/jobs/subscriptions/list-subscriptions.component.ts +++ b/packages/frontend/stalker-app/src/app/modules/jobs/subscriptions/list-subscriptions.component.ts @@ -15,7 +15,6 @@ import { Title } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; import { BehaviorSubject, combineLatest, map, shareReplay, switchMap, tap } from 'rxjs'; import { SubscriptionService, SubscriptionType } from '../../../api/jobs/subscriptions/subscriptions.service'; -import { AvatarComponent } from '../../../shared/components/avatar/avatar.component'; import { CronSubscription, EventSubscription, @@ -44,7 +43,6 @@ import { subscriptionTypes } from './subscription-templates'; imports: [ CommonModule, RouterModule, - AvatarComponent, MatIconModule, MatTableModule, MatPaginatorModule, diff --git a/packages/frontend/stalker-app/src/app/modules/ports/list-ports/list-ports.component.html b/packages/frontend/stalker-app/src/app/modules/ports/list-ports/list-ports.component.html index bf576480..088f996e 100644 --- a/packages/frontend/stalker-app/src/app/modules/ports/list-ports/list-ports.component.html +++ b/packages/frontend/stalker-app/src/app/modules/ports/list-ports/list-ports.component.html @@ -52,6 +52,30 @@ + + + Service + + {{ element.service }} + + + + + + Product + + {{ element.product }} + + + + + + Version + + {{ element.version }} + + + Host diff --git a/packages/frontend/stalker-app/src/app/modules/ports/list-ports/list-ports.component.ts b/packages/frontend/stalker-app/src/app/modules/ports/list-ports/list-ports.component.ts index aa83ff3e..b84ef641 100644 --- a/packages/frontend/stalker-app/src/app/modules/ports/list-ports/list-ports.component.ts +++ b/packages/frontend/stalker-app/src/app/modules/ports/list-ports/list-ports.component.ts @@ -82,8 +82,19 @@ import { PortsInteractionsService } from '../ports-interactions.service'; }) export class ListPortsComponent { dataLoading = true; - displayedColumns: string[] = ['select', 'port', 'ip', 'domains', 'project', 'tags', 'menu']; - filterOptions: string[] = ['host', 'port', 'project', 'tags', 'is']; + displayedColumns: string[] = [ + 'select', + 'port', + 'service', + 'product', + 'version', + 'ip', + 'domains', + 'project', + 'tags', + 'menu', + ]; + filterOptions: string[] = ['host', 'port', 'service', 'product', 'version', 'project', 'tags', 'is']; public readonly noDataMessage = $localize`:No port found|No port was found:No port found`; selection = new SelectionModel(true, []); @@ -163,6 +174,9 @@ export class ListPortsComponent { const includedTags = []; const ports = []; const hosts = []; + const services: string[] = []; + const products: string[] = []; + const versions: string[] = []; const projects = []; let blocked: boolean | null = null; @@ -192,6 +206,15 @@ export class ListPortsComponent { case 'host': if (value) hosts.push(value.trim().toLowerCase()); break; + case 'service': + if (value) services.push(value.trim().toLowerCase()); + break; + case 'product': + if (value) products.push(value.trim().toLowerCase()); + break; + case 'version': + if (value) versions.push(value.trim().toLowerCase()); + break; case 'tags': const tag = tags.find((t) => t.text.trim().toLowerCase() === value.trim().toLowerCase()); if (tag) includedTags.push(tag._id); @@ -219,6 +242,9 @@ export class ListPortsComponent { if (ports?.length) filterObject['ports'] = ports; if (hosts?.length) filterObject['hosts'] = hosts; if (projects?.length) filterObject['projects'] = projects; + if (services?.length) filterObject['services'] = services; + if (products?.length) filterObject['products'] = products; + if (versions?.length) filterObject['versions'] = versions; if (blocked !== null) filterObject['blocked'] = blocked; return filterObject; } diff --git a/packages/frontend/stalker-app/src/app/modules/ports/view-port/view-port.component.html b/packages/frontend/stalker-app/src/app/modules/ports/view-port/view-port.component.html index 5b4ba48a..5d1e676d 100644 --- a/packages/frontend/stalker-app/src/app/modules/ports/view-port/view-port.component.html +++ b/packages/frontend/stalker-app/src/app/modules/ports/view-port/view-port.component.html @@ -78,8 +78,28 @@ Service - @if (port$ | async) { + @if ((port$ | async) && port.service) { {{ port.service }} + } @else { + - + } + + + + Product + @if ((port$ | async) && port.product) { + {{ port.product }} + } @else { + - + } + + + + Version + @if ((port$ | async) && port.version) { + {{ port.version }} + } @else { + - } diff --git a/packages/frontend/stalker-app/src/app/shared/components/header/header.component.ts b/packages/frontend/stalker-app/src/app/shared/components/header/header.component.ts index c91e62f7..c6cd44be 100644 --- a/packages/frontend/stalker-app/src/app/shared/components/header/header.component.ts +++ b/packages/frontend/stalker-app/src/app/shared/components/header/header.component.ts @@ -4,7 +4,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; import { MatToolbarModule } from '@angular/material/toolbar'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { map, Observable, Subscription } from 'rxjs'; import { AuthService } from '../../../api/auth/auth.service'; import { ProjectsService } from '../../../api/projects/projects.service'; @@ -32,6 +32,7 @@ import { AvatarComponent } from '../avatar/avatar.component'; MatMenuModule, MatButtonModule, PillTagComponent, + RouterModule, ], templateUrl: './header.component.html', styleUrls: ['./header.component.scss'], diff --git a/packages/frontend/stalker-app/src/app/shared/types/ports/port.interface.ts b/packages/frontend/stalker-app/src/app/shared/types/ports/port.interface.ts index eacd141a..9c64a761 100644 --- a/packages/frontend/stalker-app/src/app/shared/types/ports/port.interface.ts +++ b/packages/frontend/stalker-app/src/app/shared/types/ports/port.interface.ts @@ -18,6 +18,8 @@ export interface Port extends PortNumber { blocked: boolean; blockedAt: number; service: string; + product: string; + version: string; } export interface ExtendedPort extends Port {