diff --git a/Astrapia/components/Graph.vue b/Astrapia/components/Graph.vue index 15120c1..7abead0 100644 --- a/Astrapia/components/Graph.vue +++ b/Astrapia/components/Graph.vue @@ -167,12 +167,14 @@ export default { this.node = this.node .data(nodes, d => d.id) - .join(enter => enter.append("circle") - .attr("r", 8) - .attr("fill", d => d.hexColor ?? "#537B87") - .call(this.drag(this.simulation)) - .call(node => node.append("title") - .text(d => `IP: ${d.name} ${d.tags ? `\nTags: ${d.tags}` : ''}`)) + .join( + enter => enter.append("circle") + .attr("r", 8) + .attr("fill", d => d.hexColor ?? "#537B87") + .call(this.drag(this.simulation)) + .call(node => node.append("title") + .text(d => `IP: ${d.name} ${d.tags ? `\nTags: ${d.tags}` : ''}`)), + update => update.attr("fill", d => d.hexColor ?? "#537B87") ); this.label = this.label @@ -255,7 +257,7 @@ export default { if (selectedNode) { this.node.transition() .duration(150) - .attr("fill", "#537B87") + .attr("fill", d => d.hexColor ?? "#537B87") .attr("r", 8) .attr("opacity", 1); this.link.transition() diff --git a/Astrapia/components/GraphFilterMenu.vue b/Astrapia/components/GraphFilterMenu.vue index 9bb8540..3fe0a2c 100644 --- a/Astrapia/components/GraphFilterMenu.vue +++ b/Astrapia/components/GraphFilterMenu.vue @@ -21,7 +21,8 @@ const props = defineProps({ const emit = defineEmits<{ layersFetched: [], - menuOpened: [boolean] + menuOpened: [boolean], + queryConditions: [any] }>(); const graphFilterMenuState = ref({ @@ -35,6 +36,7 @@ const graphFilterMenuState = ref({ async function getLayersOfLayout() { const layoutData = await layoutService.getLayoutByName(graphFilterMenuState.value.selectedLayout.name); graphFilterMenuState.value.selectedLayout.layers = layoutData.layers; + emit('queryConditions', layoutData.queryConditions); emit('layersFetched'); } diff --git a/Astrapia/components/QueryConditionButton.vue b/Astrapia/components/QueryConditionButton.vue new file mode 100644 index 0000000..04a05e7 --- /dev/null +++ b/Astrapia/components/QueryConditionButton.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/Astrapia/components/QueryConditionForm.vue b/Astrapia/components/QueryConditionForm.vue new file mode 100644 index 0000000..1541bc4 --- /dev/null +++ b/Astrapia/components/QueryConditionForm.vue @@ -0,0 +1,337 @@ + + + + + diff --git a/Astrapia/components/conditions/StyleConditionBox.vue b/Astrapia/components/conditions/StyleConditionBox.vue index 5a6d5f3..e20b970 100644 --- a/Astrapia/components/conditions/StyleConditionBox.vue +++ b/Astrapia/components/conditions/StyleConditionBox.vue @@ -74,16 +74,15 @@
- - - - - - - - - - +
+ +
+
+ +
+
+ +
@@ -208,21 +207,21 @@ const filterConditionBoxState = ref({ useProtocolColors: true, protocolColors: { Unknown:{ - startHex: "#000000", - endHex: "#FFFFFF" + startHex: "#FFFFFF", + endHex: "#000000" }, Tcp:{ - startHex: "#000000", - endHex: "#FFFFFF" + startHex: "#FFFFFF", + endHex: "#000000" }, Udp:{ - startHex: "#000000", - endHex: "#FFFFFF" + startHex: "#FFFFFF", + endHex: "#000000" }, - // Icmp:{ - // startHex: "#000000", - // endHex: "#FFFFFF" - // } + Icmp:{ + startHex: "#FFFFFF", + endHex: "#000000" + } } }, nodeStyler: { diff --git a/Astrapia/pages/topology.vue b/Astrapia/pages/topology.vue index 49f45d3..5f19d5e 100644 --- a/Astrapia/pages/topology.vue +++ b/Astrapia/pages/topology.vue @@ -2,11 +2,12 @@
- - + +
@@ -18,11 +19,13 @@ import Graph from "~/components/Graph.vue"; import Dropdown from "~/components/Dropdown.vue"; import GraphFilterMenu from "~/components/GraphFilterMenu.vue"; import TopologyTimeframeSelector from "~/components/TopologyTimeframeSelector.vue"; +import QueryConditionButton from "~/components/QueryConditionButton.vue"; const layout = ref(''); const timeframeSelectorFrom = ref(new Date(new Date().getTime() - 2 * 60 * 1000).toISOString().slice(0,16)) const timeframeSelectorTo = ref(new Date().toISOString().slice(0,16)) const data = ref(); +const queryConditions = ref(null); const intervalAmount = ref(0); let fetchInterval: NodeJS.Timeout | null = null; @@ -34,6 +37,10 @@ const handleTimeframeSelection = (from: string, to: string) => { fetchAndUpdateGraph(); } +const handleQueryConditions = (conditions: any) => { + queryConditions.value = conditions; +}; + const handleIntervalAmount = (amount: number) => { intervalAmount.value = amount; if (fetchInterval) { @@ -119,4 +126,12 @@ onBeforeUnmount(() => { z-index: 15; margin: 0.75vh 13vw 0 0; } + +.query-conditions{ + position: absolute; + top: 0; + right: 0; + z-index: 15; + margin: 0.75vh 39vw 0 0; +} diff --git a/Astrapia/plugins/fontawesome.js b/Astrapia/plugins/fontawesome.js index 92a1233..cc2e792 100644 --- a/Astrapia/plugins/fontawesome.js +++ b/Astrapia/plugins/fontawesome.js @@ -21,7 +21,8 @@ import { faMinus, faFloppyDisk, faArrowLeft, - faRightFromBracket + faRightFromBracket, + faFilter } from '@fortawesome/free-solid-svg-icons' library.add( @@ -45,7 +46,8 @@ library.add( faMinus, faFloppyDisk, faArrowLeft, - faRightFromBracket + faRightFromBracket, + faFilter ) config.autoAddCss = false diff --git a/Astrapia/services/layoutService.ts b/Astrapia/services/layoutService.ts index 64c597d..69c7ee7 100644 --- a/Astrapia/services/layoutService.ts +++ b/Astrapia/services/layoutService.ts @@ -7,7 +7,13 @@ export interface Layouts { export interface Layout { name: string, - layers: [] + layers: [], + queryConditions: { + allowDuplicates: boolean, + dataProtocolsWhitelist: [], + flowProtocolsWhitelist: [], + portsWhitelist: [] + } } class LayoutService { @@ -30,6 +36,10 @@ class LayoutService { public updateLayout(name: string, newName: string) { return ApiService.put(`/api/layout/${name}/${newName}`); } + + public setQueryConditions(name: string, queryConditions: any){ + return ApiService.put(`/api/layout/${name}/queryConditions`, queryConditions) + } } export default new LayoutService(); diff --git a/Packrat/Fennec.Tests/Integration/QueryConditionTests.cs b/Packrat/Fennec.Tests/Integration/QueryConditionTests.cs index 7c4bc7f..4c69ccf 100644 --- a/Packrat/Fennec.Tests/Integration/QueryConditionTests.cs +++ b/Packrat/Fennec.Tests/Integration/QueryConditionTests.cs @@ -54,8 +54,8 @@ public async Task FiltersByPort() await SeedDatabase(); var conditions = new QueryConditions { - FlowProtocolsWhitelist = new[] { FlowProtocol.Ipfix, FlowProtocol.Netflow5 }, - DataProtocolsWhitelist = new[] { DataProtocol.Tcp } + FlowProtocolsWhitelist = new List { FlowProtocol.Ipfix, FlowProtocol.Netflow5 }, + DataProtocolsWhitelist = new List { DataProtocol.Tcp } }; var service = new TraceRepository(Database, null!, null!); @@ -77,7 +77,7 @@ public async Task FiltersByDuplicateAndPort() var conditions = new QueryConditions { AllowDuplicates = false, - PortsWhitelist = new[] { 10, 50 } + PortsWhitelist = new List { 10, 50 } }; var service = new TraceRepository(Database, null!, null!); diff --git a/Packrat/Fennec/Controllers/LayoutController.cs b/Packrat/Fennec/Controllers/LayoutController.cs index 536d7cc..574ebbd 100644 --- a/Packrat/Fennec/Controllers/LayoutController.cs +++ b/Packrat/Fennec/Controllers/LayoutController.cs @@ -50,6 +50,9 @@ public async Task List() [SwaggerResponse(StatusCodes.Status400BadRequest, "A layout with the name already exists")] public async Task Create(string name) { + if (!ModelState.IsValid) + return BadRequest(); + try { var layout = await _layoutRepository.CreateLayout(name); @@ -68,7 +71,7 @@ public async Task Create(string name) /// /// [HttpGet("{name}")] - [SwaggerResponse(StatusCodes.Status200OK, "Layout successfully returned", typeof(Layout))] + [SwaggerResponse(StatusCodes.Status200OK, "Layout successfully returned", typeof(FullLayoutDto))] [SwaggerResponse(StatusCodes.Status404NotFound, "The layout with the name does not exist")] public async Task Get(string name) { @@ -137,14 +140,20 @@ public async Task Delete(string name) [HttpPut("{name}/queryConditions")] [SwaggerResponse(StatusCodes.Status200OK, "Query conditions successfully updated", typeof(FullLayoutDto))] [SwaggerResponse(StatusCodes.Status404NotFound, "The layout with the name does not exist")] - public async Task ReplaceQueryConditions(string name, QueryConditionsDto queryConditions) + public async Task ReplaceQueryConditions(string name, [FromBody] QueryConditionsDto queryConditions) { + if (!ModelState.IsValid) + return BadRequest(); + var layout = await _layoutRepository.GetLayout(name); if (layout == null) return NotFound($"The layout with the name `{name}` does not exist."); var newQueryConditions = _mapper.Map(queryConditions); await _layoutRepository.ReplaceQueryConditions(name, newQueryConditions); + + // TODO: update it using the previous call instead of re-fetching + layout = await _layoutRepository.GetLayout(name); return Ok(_mapper.Map(layout)); } } diff --git a/Packrat/Fennec/Database/Domain/Layout.cs b/Packrat/Fennec/Database/Domain/Layout.cs index bc606c1..5d9cb04 100644 --- a/Packrat/Fennec/Database/Domain/Layout.cs +++ b/Packrat/Fennec/Database/Domain/Layout.cs @@ -27,28 +27,28 @@ public class QueryConditions /// /// This is intended to help differentiate between SFlow and other flow protocols. [BsonElement("flowProtocolsWhitelist")] - public FlowProtocol[]? FlowProtocolsWhitelist { get; set; } + public List? FlowProtocolsWhitelist { get; set; } /// /// A list of allowed only those data carrying protocols that should be included in the result. If null this /// condition is ignored. /// [BsonElement("dataProtocolsWhitelist")] - public DataProtocol[]? DataProtocolsWhitelist { get; set; } + public List? DataProtocolsWhitelist { get; set; } /// /// If specified a list of allowed source and destination ports that should be included in the result. If either /// source or destination port matches any of the ports in the list the trace is included in the result. /// [BsonElement("portsWhitelist")] - public int[]? PortsWhitelist { get; set; } + public List? PortsWhitelist { get; set; } } public record QueryConditionsDto( bool? AllowDuplicates, - FlowProtocol[]? FlowProtocolsWhitelist, - DataProtocol[]? DataProtocolsWhitelist, - int[]? PortsWhitelist); + List? FlowProtocolsWhitelist, + List? DataProtocolsWhitelist, + List? PortsWhitelist); /// /// Represents a list of steps that should be taken before sending data to the frontend. @@ -59,6 +59,7 @@ public Layout(string name) { Name = name; Layers = new List(); + QueryConditions = new QueryConditions(); } #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. @@ -78,7 +79,7 @@ protected Layout() { } /// /// The conditions that should be applied when querying the database. /// - [BsonElement("queryConditions")] + [BsonElement("queryConditions")] public QueryConditions QueryConditions { get; set; } = new(); /// diff --git a/Packrat/Fennec/Database/MapperProfile.cs b/Packrat/Fennec/Database/MapperProfile.cs index bcaaac5..77a3320 100644 --- a/Packrat/Fennec/Database/MapperProfile.cs +++ b/Packrat/Fennec/Database/MapperProfile.cs @@ -12,16 +12,16 @@ public MapperProfile() // TODO: rethink all this dto madness CreateMap() .ConvertUsing(ip => ip.ToString()); - + CreateMap() .ConvertUsing(str => IPAddress.Parse(str)); - + CreateMap() .ConvertUsing(str => IPAddress.Parse(str).GetAddressBytes()); - + CreateMap() .ConvertUsing(bytes => new IPAddress(bytes).ToString()); - + CreateMap() .ConstructUsing((dto, _) => { @@ -63,36 +63,48 @@ public MapperProfile() CreateMap(); CreateMap(); - + CreateMap() .ConstructUsing((dto, ctx) => { if (!LayerType.LookupTable.TryGetValue(dto.Type, out var layerType)) throw new ArgumentException("No layer type found in the lookup table."); - - return (ILayer) ctx.Mapper.Map(dto, dto.GetType(), layerType.LayerType); + + return (ILayer)ctx.Mapper.Map(dto, dto.GetType(), layerType.LayerType); }); - + CreateMap() .ConstructUsing((layer, ctx) => { if (!LayerType.LookupTable.TryGetValue(layer.Type, out var layerType)) throw new ArgumentException("No layer type found in the lookup table."); - - return (ILayerDto) ctx.Mapper.Map(layer, layer.GetType(), layerType.DtoType); + + return (ILayerDto)ctx.Mapper.Map(layer, layer.GetType(), layerType.DtoType); }); CreateMap() - .ForCtorParam("LayerCount", opt => + .ForCtorParam("LayerCount", opt => opt.MapFrom(src => src.Layers.Count)); + CreateMap() + .ForAllMembers(opts => opts.AllowNull()); - CreateMap(); CreateMap() - .ForAllMembers(opts => opts.AllowNull()); + .ConstructUsing((q, _) => + new QueryConditionsDto(q.AllowDuplicates, q.FlowProtocolsWhitelist, q.DataProtocolsWhitelist, + q.PortsWhitelist)) + .ForAllMembers(o => o.AllowNull()); CreateMap() - .ForAllMembers(opts => opts.AllowNull()); + .ConstructUsing(q => + new QueryConditions + { + AllowDuplicates = q.AllowDuplicates, + FlowProtocolsWhitelist = q.FlowProtocolsWhitelist, + DataProtocolsWhitelist = q.DataProtocolsWhitelist, + PortsWhitelist = q.PortsWhitelist + }) + .ForAllMembers(o => o.AllowNull()); CreateMap(); CreateMap(); @@ -101,7 +113,7 @@ public MapperProfile() CreateMap(); CreateMap(); - + CreateMap(); CreateMap(); CreateMap(); diff --git a/Packrat/Fennec/Startup.cs b/Packrat/Fennec/Startup.cs index 1a518a2..c24c11d 100644 --- a/Packrat/Fennec/Startup.cs +++ b/Packrat/Fennec/Startup.cs @@ -1,6 +1,7 @@ using System.Data; using System.Net.Http.Headers; using System.Text; +using System.Text.Json.Serialization; using Fennec.Database; using Fennec.Database.Domain; using Fennec.Metrics; @@ -49,10 +50,7 @@ public void ConfigureServices(IServiceCollection services, IWebHostEnvironment e { BsonSerializer.RegisterSerializer(typeof(ILayer), new MongoLayerSerializer()); BsonSerializer.RegisterSerializer(typeof(Dictionary), new ProtocolColorsDictionarySerializer()); - JsonConvert.DefaultSettings = () => new JsonSerializerSettings - { - Converters = { new StringEnumConverter() } - }; + services.AddAutoMapper(typeof(MapperProfile)); @@ -174,8 +172,14 @@ public void ConfigureServices(IServiceCollection services, IWebHostEnvironment e Log.Error("Failed to read multiplexer configuration... To run no multiplexers define an empty list"); // Web services - services.AddControllers(c => { c.ModelBinderProviders.Insert(0, new LayerModelBinderProvider()); }) - .AddNewtonsoftJson(options => { options.SerializerSettings.Converters.Add(new StringEnumConverter()); }); + services + .AddControllers(c => { c.ModelBinderProviders.Insert(0, new LayerModelBinderProvider()); }) + .AddNewtonsoftJson(options => + { + options.SerializerSettings.NullValueHandling = NullValueHandling.Include; + options.SerializerSettings.Converters.Add(new StringEnumConverter()); + }); + services.AddAutoMapper(typeof(Program).Assembly); if (StartupOptions.AllowCors)