|
173 | 173 | } |
174 | 174 | } |
175 | 175 | } else { |
176 | | - output.append(s"Found ${flows.size} taint flow(s):\n\n") |
177 | | - |
178 | | - flows.zipWithIndex.foreach { case (flow, idx) => |
179 | | - output.append(s"--- Flow ${idx + 1} ---\n") |
180 | | - |
| 176 | + // Deduplicate: track seen (source_file:line -> sink_file:line) pairs |
| 177 | + val seen = mutable.Set[String]() |
| 178 | + |
| 179 | + flows.foreach { flow => |
181 | 180 | val elements = flow.elements.l |
182 | 181 | if (elements.nonEmpty) { |
183 | | - // Source (first element) |
184 | 182 | val source = elements.head |
| 183 | + val sink = elements.last |
| 184 | + |
185 | 185 | val srcFile = source.file.name.headOption.getOrElse("?") |
186 | 186 | val srcLine = source.lineNumber.getOrElse(-1) |
187 | | - val srcMethod = getMethodName(source) |
188 | | - output.append(s"Source: ${source.code}\n") |
189 | | - output.append(s" Location: $srcFile:$srcLine in $srcMethod()\n") |
190 | | - |
191 | | - // Path elements (intermediate steps) |
192 | | - if (elements.size > 2) { |
193 | | - output.append(s"\nPath (${elements.size - 2} intermediate steps):\n") |
194 | | - elements.slice(1, elements.size - 1).take(15).foreach { elem => |
195 | | - val elemFile = elem.file.name.headOption.getOrElse("?") |
196 | | - val elemLine = elem.lineNumber.getOrElse(-1) |
197 | | - val elemMethod = getMethodName(elem) |
198 | | - val codeSnippet = elem.code.take(60).replaceAll("\n", " ") |
199 | | - output.append(s" [$elemFile:$elemLine] $codeSnippet\n") |
200 | | - output.append(s" in $elemMethod()\n") |
201 | | - } |
202 | | - if (elements.size - 2 > 15) { |
203 | | - output.append(s" ... and ${elements.size - 17} more steps\n") |
204 | | - } |
205 | | - } |
206 | | - |
207 | | - // Sink (last element) |
208 | | - val sink = elements.last |
209 | 187 | val snkFile = sink.file.name.headOption.getOrElse("?") |
210 | 188 | val snkLine = sink.lineNumber.getOrElse(-1) |
211 | | - val snkMethod = getMethodName(sink) |
212 | | - output.append(s"\nSink: ${sink.code}\n") |
213 | | - output.append(s" Location: $snkFile:$snkLine in $snkMethod()\n") |
214 | | - |
215 | | - output.append(s"\nPath length: ${elements.size} nodes\n") |
| 189 | + |
| 190 | + val key = s"$srcFile:$srcLine->$snkFile:$snkLine" |
| 191 | + if (!seen.contains(key)) { |
| 192 | + seen.add(key) |
| 193 | + |
| 194 | + output.append(s"--- Flow ${seen.size} ---\n") |
| 195 | + |
| 196 | + // Source (first element) |
| 197 | + val srcMethod = getMethodName(source) |
| 198 | + output.append(s"Source: ${source.code}\n") |
| 199 | + output.append(s" Location: $srcFile:$srcLine in $srcMethod()\n") |
| 200 | + |
| 201 | + // Path elements (intermediate steps) |
| 202 | + if (elements.size > 2) { |
| 203 | + output.append(s"\nPath (${elements.size - 2} intermediate steps):\n") |
| 204 | + elements.slice(1, elements.size - 1).take(15).foreach { elem => |
| 205 | + val elemFile = elem.file.name.headOption.getOrElse("?") |
| 206 | + val elemLine = elem.lineNumber.getOrElse(-1) |
| 207 | + val elemMethod = getMethodName(elem) |
| 208 | + val codeSnippet = elem.code.take(60).replaceAll("\n", " ") |
| 209 | + output.append(s" [$elemFile:$elemLine] $codeSnippet\n") |
| 210 | + output.append(s" in $elemMethod()\n") |
| 211 | + } |
| 212 | + if (elements.size - 2 > 15) { |
| 213 | + output.append(s" ... and ${elements.size - 17} more steps\n") |
| 214 | + } |
| 215 | + } |
| 216 | + |
| 217 | + // Sink (last element) |
| 218 | + val snkMethod = getMethodName(sink) |
| 219 | + output.append(s"\nSink: ${sink.code}\n") |
| 220 | + output.append(s" Location: $snkFile:$snkLine in $snkMethod()\n") |
| 221 | + |
| 222 | + output.append(s"\nPath length: ${elements.size} nodes\n\n") |
| 223 | + } |
216 | 224 | } |
217 | | - output.append("\n") |
218 | 225 | } |
| 226 | + |
| 227 | + output.append(s"Summary: ${seen.size} unique flow(s)\n") |
219 | 228 | } |
220 | 229 | } |
221 | 230 |
|
|
0 commit comments