@@ -17,7 +17,8 @@ const state = {
1717 sessionFilter : "all" ,
1818 docsQuery : "" ,
1919 theme : "light" ,
20- bootstrap : { }
20+ bootstrap : { } ,
21+ selectedGraphNode : null
2122} ;
2223
2324const fields = [
@@ -245,6 +246,7 @@ function fillForm(session) {
245246 el ( "verbose" ) . checked = config . verbose !== false ;
246247 el ( "adaptive-rate" ) . checked = config . adaptive_rate !== false ;
247248 state . dirty = false ;
249+ state . selectedGraphNode = null ;
248250 syncModeControls ( ) ;
249251 renderBuilderNotes ( formData ( ) ) ;
250252 renderCommandPreview ( formData ( ) ) ;
@@ -561,10 +563,24 @@ function renderSnapshot(session) {
561563
562564function renderReports ( session ) {
563565 const artifacts = session ?. artifacts || { } ;
566+ const readyCount = Object . keys ( artifactCatalog ) . filter ( ( key ) => Boolean ( artifacts [ key ] ) ) . length ;
567+ const reportWarnings = Array . isArray ( artifacts . report_errors ) ? artifacts . report_errors : [ ] ;
564568 const links = Object . entries ( artifactCatalog )
565569 . filter ( ( [ key ] ) => Boolean ( artifacts [ key ] ) )
566570 . map ( ( [ key , [ label , copy , meta ] ] ) => [ label , key , copy , meta ] ) ;
567571
572+ el ( "report-summary" ) . innerHTML = [
573+ [ "Preferred output" , labelForFormat ( session ?. config ?. format || "html" ) ] ,
574+ [ "Report engine" , session ?. payload ?. meta ?. report_engine || session ?. meta ?. report_engine || "ASRFacet-Rb" ] ,
575+ [ "Artifacts ready" , String ( readyCount ) ] ,
576+ [ "Render warnings" , reportWarnings . length > 0 ? String ( reportWarnings . length ) : "None" ]
577+ ] . map ( ( [ label , value ] ) => `
578+ <div class="summary-tile">
579+ <span>${ escapeHtml ( label ) } </span>
580+ <strong>${ escapeHtml ( value ) } </strong>
581+ </div>
582+ ` ) . join ( "" ) ;
583+
568584 el ( "report-links" ) . innerHTML = links . map ( ( [ label , key , copy , meta ] ) => `
569585 <a class="button button-secondary report-card" target="_blank" rel="noopener" href="/reports/${ encodeURIComponent ( session . id ) } /${ key } ">
570586 <span class="report-card-title">
@@ -574,7 +590,20 @@ function renderReports(session) {
574590 <span class="report-card-copy">${ escapeHtml ( copy ) } </span>
575591 <span class="report-card-meta">${ escapeHtml ( meta ) } </span>
576592 </a>
577- ` ) . join ( "" ) || '<div class="notice">Reports appear here after the first completed run.</div>' ;
593+ ` ) . join ( "" ) ;
594+
595+ if ( reportWarnings . length > 0 ) {
596+ el ( "report-links" ) . innerHTML += reportWarnings . map ( ( warning ) => `
597+ <div class="notice">
598+ <strong>${ escapeHtml ( String ( warning . format || "report" ) . toUpperCase ( ) ) } warning</strong>
599+ <div>${ escapeHtml ( warning . message || "A renderer reported a recoverable issue." ) } </div>
600+ </div>
601+ ` ) . join ( "" ) ;
602+ }
603+
604+ if ( ! el ( "report-links" ) . innerHTML . trim ( ) ) {
605+ el ( "report-links" ) . innerHTML = '<div class="notice">Reports appear here after the first completed run.</div>' ;
606+ }
578607
579608 el ( "meta-reports" ) . textContent = links . length > 0 ? `${ links . length } ready` : "Awaiting" ;
580609}
@@ -716,53 +745,159 @@ function extractGraphModel(session) {
716745
717746 const nodes = [ ] ;
718747 const edges = [ ] ;
748+ const stats = summaryFor ( session ) ;
719749 const columns = {
720750 target : { x : 120 } ,
721751 host : { x : 330 } ,
722752 service : { x : 560 } ,
723753 finding : { x : 790 }
724754 } ;
725755
726- nodes . push ( { id : "target" , label : target , type : "target" , x : columns . target . x , y : 280 } ) ;
756+ nodes . push ( {
757+ id : "target" ,
758+ label : target ,
759+ type : "target" ,
760+ x : columns . target . x ,
761+ y : 280 ,
762+ title : "Primary target" ,
763+ detail : `This is the current session target. The session has ${ stats . subdomains || 0 } discovered hosts and ${ stats . open_ports || 0 } open ports.` ,
764+ recommendations : [ "Validate scope and exclusions before starting broader active work." ]
765+ } ) ;
727766
728767 hosts . forEach ( ( host , index ) => {
729768 const nodeId = `host-${ index } ` ;
730- nodes . push ( { id : nodeId , label : host , type : "host" , x : columns . host . x , y : 110 + ( index * 90 ) } ) ;
769+ nodes . push ( {
770+ id : nodeId ,
771+ label : host ,
772+ type : "host" ,
773+ x : columns . host . x ,
774+ y : 110 + ( index * 90 ) ,
775+ title : "Discovered host" ,
776+ detail : `${ host } is part of the collected host surface for this session.` ,
777+ recommendations : [ "Open the HTML or JSON report to inspect linked services and findings for this host." ]
778+ } ) ;
731779 edges . push ( { from : "target" , to : nodeId } ) ;
732780 } ) ;
733781
734782 ports . forEach ( ( service , index ) => {
735783 const nodeId = `service-${ index } ` ;
736- nodes . push ( { id : nodeId , label : service , type : "service" , x : columns . service . x , y : 150 + ( index * 80 ) } ) ;
784+ nodes . push ( {
785+ id : nodeId ,
786+ label : service ,
787+ type : "service" ,
788+ x : columns . service . x ,
789+ y : 150 + ( index * 80 ) ,
790+ title : "Observed service" ,
791+ detail : `${ service } represents a reachable network service observed in the current result set.` ,
792+ recommendations : [ "Use service output and banner data first, then move into HTTP or certificate review when applicable." ]
793+ } ) ;
737794 edges . push ( { from : hosts [ index % Math . max ( 1 , hosts . length ) ] ? `host-${ index % Math . max ( 1 , hosts . length ) } ` : "target" , to : nodeId } ) ;
738795 } ) ;
739796
740797 findings . forEach ( ( finding , index ) => {
741798 const nodeId = `finding-${ index } ` ;
742- nodes . push ( { id : nodeId , label : finding , type : "finding" , x : columns . finding . x , y : 180 + ( index * 90 ) } ) ;
799+ nodes . push ( {
800+ id : nodeId ,
801+ label : finding ,
802+ type : "finding" ,
803+ x : columns . finding . x ,
804+ y : 180 + ( index * 90 ) ,
805+ title : "Generated finding" ,
806+ detail : `${ finding } is a heuristic or correlated issue worth manual validation.` ,
807+ recommendations : [ "Confirm severity and ownership before escalating or ticketing the finding." ]
808+ } ) ;
743809 edges . push ( { from : ports [ index % Math . max ( 1 , ports . length ) ] ? `service-${ index % Math . max ( 1 , ports . length ) } ` : "target" , to : nodeId } ) ;
744810 } ) ;
745811
746812 ips . slice ( 0 , 3 ) . forEach ( ( ip , index ) => {
747813 const nodeId = `ip-${ index } ` ;
748- nodes . push ( { id : nodeId , label : ip , type : "host" , x : columns . host . x , y : 390 + ( index * 70 ) } ) ;
814+ nodes . push ( {
815+ id : nodeId ,
816+ label : ip ,
817+ type : "host" ,
818+ x : columns . host . x ,
819+ y : 390 + ( index * 70 ) ,
820+ title : "Resolved IP" ,
821+ detail : `${ ip } is a resolved or directly scanned IP tied to this session.` ,
822+ recommendations : [ "Check ownership and hosting context before treating shared infrastructure as fully in scope." ]
823+ } ) ;
749824 edges . push ( { from : "target" , to : nodeId } ) ;
750825 } ) ;
751826
752827 return { nodes, edges } ;
753828}
754829
830+ function buildGraphSelection ( model , selectedId ) {
831+ const selectedNode = model . nodes . find ( ( node ) => node . id === selectedId ) || model . nodes [ 0 ] ;
832+ const connectedIds = new Set ( [ selectedNode ?. id ] . filter ( Boolean ) ) ;
833+ model . edges . forEach ( ( edge ) => {
834+ if ( edge . from === selectedNode ?. id || edge . to === selectedNode ?. id ) {
835+ connectedIds . add ( edge . from ) ;
836+ connectedIds . add ( edge . to ) ;
837+ }
838+ } ) ;
839+
840+ return { selectedNode, connectedIds } ;
841+ }
842+
843+ function renderGraphFocus ( session , model , selection ) {
844+ const summary = summaryFor ( session ) ;
845+ const selectedNode = selection . selectedNode ;
846+ const linkedNodes = model . nodes . filter ( ( node ) => selection . connectedIds . has ( node . id ) && node . id !== selectedNode ?. id ) ;
847+
848+ if ( ! selectedNode ) {
849+ el ( "graph-focus" ) . innerHTML = `
850+ <div class="focus-block">
851+ <div class="eyebrow">Graph Summary</div>
852+ <strong>${ escapeHtml ( session ?. name || "Untitled session" ) } </strong>
853+ <div class="nav-hint">${ escapeHtml ( session ?. config ?. target || "No target selected" ) } </div>
854+ </div>
855+ ` ;
856+ return ;
857+ }
858+
859+ el ( "graph-focus" ) . innerHTML = `
860+ <div class="focus-block">
861+ <div class="eyebrow">Selected Node</div>
862+ <strong>${ escapeHtml ( selectedNode . label ) } </strong>
863+ <div class="nav-hint">${ escapeHtml ( selectedNode . title || "Graph entity" ) } </div>
864+ </div>
865+ <div class="focus-block">
866+ <div class="eyebrow">Meaning</div>
867+ <strong>${ escapeHtml ( selectedNode . detail || "No extra detail is available for this node." ) } </strong>
868+ </div>
869+ <div class="focus-block">
870+ <div class="eyebrow">Linked Entities</div>
871+ <strong>${ escapeHtml ( String ( linkedNodes . length ) ) } directly connected node${ linkedNodes . length === 1 ? "" : "s" } </strong>
872+ <ul class="focus-list">
873+ ${ ( linkedNodes . length > 0 ? linkedNodes : [ { label : "No direct neighbors" , type : "idle" } ] ) . map ( ( node ) => `<li>${ escapeHtml ( node . label ) } ${ node . type ? ` (${ escapeHtml ( node . type ) } )` : "" } </li>` ) . join ( "" ) }
874+ </ul>
875+ </div>
876+ <div class="focus-block">
877+ <div class="eyebrow">Recommended Next Move</div>
878+ <strong>${ escapeHtml ( ( selectedNode . recommendations || [ ] ) [ 0 ] || "Use the linked reports and event stream to continue inspection." ) } </strong>
879+ </div>
880+ <div class="focus-block">
881+ <div class="eyebrow">Session Context</div>
882+ <strong>${ escapeHtml ( `${ summary . subdomains || 0 } hosts, ${ summary . open_ports || 0 } services, ${ summary . findings || 0 } findings are currently represented in this session.` ) } </strong>
883+ </div>
884+ ` ;
885+ }
886+
755887function renderGraph ( session ) {
756888 const model = extractGraphModel ( session ) ;
889+ const selection = buildGraphSelection ( model , state . selectedGraphNode || "target" ) ;
757890 const edgeMarkup = model . edges . map ( ( edge ) => {
758891 const from = model . nodes . find ( ( node ) => node . id === edge . from ) ;
759892 const to = model . nodes . find ( ( node ) => node . id === edge . to ) ;
760893 if ( ! from || ! to ) return "" ;
761- return `<line class="graph-edge" x1="${ from . x } " y1="${ from . y } " x2="${ to . x } " y2="${ to . y } "></line>` ;
894+ const edgeConnected = edge . from === selection . selectedNode ?. id || edge . to === selection . selectedNode ?. id ;
895+ const edgeClass = edgeConnected ? "graph-edge connected" : ( selection . selectedNode ? "graph-edge dimmed" : "graph-edge" ) ;
896+ return `<line class="${ edgeClass } " x1="${ from . x } " y1="${ from . y } " x2="${ to . x } " y2="${ to . y } "></line>` ;
762897 } ) . join ( "" ) ;
763898
764899 const nodeMarkup = model . nodes . map ( ( node ) => `
765- <g class="graph-node ${ escapeHtml ( node . type ) } ">
900+ <g class="graph-node ${ escapeHtml ( node . type ) } ${ node . id === selection . selectedNode ?. id ? "active" : "" } ${ selection . connectedIds . has ( node . id ) ? "connected" : "dimmed" } " data-node-id=" ${ escapeHtml ( node . id ) } ">
766901 <circle cx="${ node . x } " cy="${ node . y } " r="${ node . type === "target" ? 34 : 28 } "></circle>
767902 <text x="${ node . x } " y="${ node . y + 4 } " text-anchor="middle">${ escapeHtml ( node . label . slice ( 0 , 18 ) ) } </text>
768903 </g>
@@ -774,27 +909,13 @@ function renderGraph(session) {
774909 ${ nodeMarkup }
775910 </svg>
776911 ` ;
777-
778- const summary = summaryFor ( session ) ;
779- el ( "graph-focus" ) . innerHTML = `
780- <div class="focus-block">
781- <div class="eyebrow">Graph Summary</div>
782- <strong>${ escapeHtml ( session ?. name || "Untitled session" ) } </strong>
783- <div class="nav-hint">${ escapeHtml ( session ?. config ?. target || "No target selected" ) } </div>
784- </div>
785- <div class="focus-block">
786- <div class="eyebrow">Connected counts</div>
787- <strong>${ escapeHtml ( String ( summary . subdomains || 0 ) ) } hosts, ${ escapeHtml ( String ( summary . open_ports || 0 ) ) } services, ${ escapeHtml ( String ( summary . findings || 0 ) ) } findings</strong>
788- </div>
789- <div class="focus-block">
790- <div class="eyebrow">Interpretation</div>
791- <strong>${ summary . findings > 0 ? "The graph is showing service-to-finding pivots so the likely review path is visible." : "The graph is showing target-to-host and host-to-service relationships for infrastructure review." } </strong>
792- </div>
793- <div class="focus-block">
794- <div class="eyebrow">Operator note</div>
795- <strong>Use this tab to reason about pivots and ownership relationships before opening the detailed reports.</strong>
796- </div>
797- ` ;
912+ Array . from ( el ( "graph-canvas" ) . querySelectorAll ( "[data-node-id]" ) ) . forEach ( ( node ) => {
913+ node . addEventListener ( "click" , ( ) => {
914+ state . selectedGraphNode = node . getAttribute ( "data-node-id" ) ;
915+ renderGraph ( session ) ;
916+ } ) ;
917+ } ) ;
918+ renderGraphFocus ( session , model , selection ) ;
798919}
799920
800921function syncChrome ( session ) {
0 commit comments