@@ -36,6 +36,119 @@ class DockerCollector extends BaseCollector {
3636 return ip ;
3737 }
3838
39+ _getDockerLogicalKey ( entry ) {
40+ if ( ! entry || entry . source !== "docker" || ! entry . container_id || ! entry . host_port ) {
41+ return null ;
42+ }
43+ const hostPort = parseInt ( entry . host_port , 10 ) ;
44+ if ( Number . isNaN ( hostPort ) || hostPort <= 0 ) {
45+ return null ;
46+ }
47+ const containerId = String ( entry . container_id ) . substring ( 0 , 12 ) ;
48+ const protocol = entry . protocol || "tcp" ;
49+ return `${ containerId } :${ hostPort } :${ protocol } ` ;
50+ }
51+
52+ _selectPreferredDockerPortEntry ( existingEntry , nextEntry ) {
53+ if ( ! existingEntry ) return nextEntry ;
54+ if ( ! nextEntry ) return existingEntry ;
55+ const score = ( entry ) => {
56+ let value = 0 ;
57+ const normalizedIp = this . _normalizeHostIp ( entry . host_ip ) ;
58+ if ( normalizedIp === "0.0.0.0" ) value += 3 ;
59+ if ( normalizedIp === "127.0.0.1" ) value += 2 ;
60+ if ( ! entry . internal ) value += 1 ;
61+ return value ;
62+ } ;
63+ return score ( nextEntry ) > score ( existingEntry ) ? nextEntry : existingEntry ;
64+ }
65+
66+ _collapseDockerLogicalDuplicates ( entries ) {
67+ const passthroughEntries = [ ] ;
68+ const publishedDockerEntries = new Map ( ) ;
69+ const internalDockerEntries = new Map ( ) ;
70+
71+ entries . forEach ( ( rawEntry ) => {
72+ const entry = this . normalizePortEntry ( rawEntry ) ;
73+ const logicalKey = this . _getDockerLogicalKey ( entry ) ;
74+ if ( ! logicalKey ) {
75+ passthroughEntries . push ( entry ) ;
76+ return ;
77+ }
78+ if ( entry . internal ) {
79+ internalDockerEntries . set (
80+ logicalKey ,
81+ this . _selectPreferredDockerPortEntry ( internalDockerEntries . get ( logicalKey ) , entry )
82+ ) ;
83+ return ;
84+ }
85+ publishedDockerEntries . set (
86+ logicalKey ,
87+ this . _selectPreferredDockerPortEntry ( publishedDockerEntries . get ( logicalKey ) , entry )
88+ ) ;
89+ } ) ;
90+
91+ const collapsedEntries = [ ...passthroughEntries ] ;
92+ for ( const entry of publishedDockerEntries . values ( ) ) {
93+ collapsedEntries . push ( entry ) ;
94+ }
95+ for ( const [ logicalKey , entry ] of internalDockerEntries . entries ( ) ) {
96+ if ( ! publishedDockerEntries . has ( logicalKey ) ) {
97+ collapsedEntries . push ( entry ) ;
98+ }
99+ }
100+ return collapsedEntries ;
101+ }
102+
103+ _getProcessLogicalKey ( entry ) {
104+ if ( ! entry || ! entry . host_port ) {
105+ return null ;
106+ }
107+ const hostPort = parseInt ( entry . host_port , 10 ) ;
108+ if ( Number . isNaN ( hostPort ) || hostPort <= 0 ) {
109+ return null ;
110+ }
111+ const protocol = entry . protocol || "tcp" ;
112+ const owner = String ( entry . owner || "" ) . trim ( ) . toLowerCase ( ) ;
113+ if ( ! owner || owner === "unknown" ) {
114+ const pid =
115+ entry . pid ||
116+ ( Array . isArray ( entry . pids ) && entry . pids . length > 0 ? entry . pids [ 0 ] : null ) ;
117+ if ( pid ) {
118+ return `pid:${ pid } :${ hostPort } :${ protocol } ` ;
119+ }
120+ if ( entry . source === "system" || ( entry . source === "docker" && ! entry . container_id ) ) {
121+ return `unknown:${ entry . source || "system" } :${ hostPort } :${ protocol } ` ;
122+ }
123+ return null ;
124+ }
125+ return `owner:${ owner } :${ hostPort } :${ protocol } ` ;
126+ }
127+
128+ _collapseProcessLogicalDuplicates ( entries ) {
129+ const passthroughEntries = [ ] ;
130+ const processEntries = new Map ( ) ;
131+
132+ entries . forEach ( ( rawEntry ) => {
133+ const entry = this . normalizePortEntry ( rawEntry ) ;
134+ if ( entry . source === "docker" && entry . container_id ) {
135+ passthroughEntries . push ( entry ) ;
136+ return ;
137+ }
138+ const logicalKey = this . _getProcessLogicalKey ( entry ) ;
139+ if ( ! logicalKey ) {
140+ passthroughEntries . push ( entry ) ;
141+ return ;
142+ }
143+ processEntries . set (
144+ logicalKey ,
145+ this . _selectPreferredDockerPortEntry ( processEntries . get ( logicalKey ) , entry )
146+ ) ;
147+ } ) ;
148+
149+ return [ ...passthroughEntries , ...processEntries . values ( ) ] ;
150+ }
151+
39152 async initialize ( ) {
40153 return await this . dockerApi . connect ( ) ;
41154 }
@@ -266,8 +379,11 @@ class DockerCollector extends BaseCollector {
266379 this . logWarn ( "Failed to collect and process system ports:" , systemErr . message ) ;
267380 }
268381
269- this . logInfo ( `Total unique ports collected: ${ allPorts . length } ` ) ;
270- return allPorts ;
382+ const collapsedPorts = this . _collapseDockerLogicalDuplicates ( allPorts ) ;
383+ const deduplicatedPorts =
384+ this . _collapseProcessLogicalDuplicates ( collapsedPorts ) ;
385+ this . logInfo ( `Total unique ports collected: ${ deduplicatedPorts . length } ` ) ;
386+ return deduplicatedPorts ;
271387 } catch ( err ) {
272388 this . logError ( "Critical error in getPorts:" , err . message , err . stack ) ;
273389 return [
0 commit comments