Skip to content

Commit f339a84

Browse files
committed
better outputs
1 parent e648b9c commit f339a84

53 files changed

Lines changed: 2770 additions & 24 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

asrfacet-rb.gemspec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ Gem::Specification.new do |spec|
4646
spec.add_runtime_dependency "parallel", ">= 1.22", "< 2"
4747
spec.add_runtime_dependency "webrick", ">= 1.7", "< 2"
4848
spec.add_runtime_dependency "concurrent-ruby", ">= 1.2", "< 2"
49+
spec.add_runtime_dependency "csv", ">= 3.2", "< 4"
50+
spec.add_runtime_dependency "caracal", ">= 1.4", "< 2"
51+
spec.add_runtime_dependency "hexapdf", ">= 0.24", "< 0.25"
4952
spec.add_development_dependency "rake"
5053
spec.add_development_dependency "rspec"
5154
spec.add_development_dependency "webmock"

lib/asrfacet_rb.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,25 @@
9595
require_relative "asrfacet_rb/busters/vhost_buster"
9696
require_relative "asrfacet_rb/pipeline"
9797
require_relative "asrfacet_rb/output/base_formatter"
98+
require_relative "asrfacet_rb/output/runtime_detector"
99+
require_relative "asrfacet_rb/output/base_renderer"
100+
require_relative "asrfacet_rb/output/chart_data_builder"
98101
require_relative "asrfacet_rb/output/cli_formatter"
99102
require_relative "asrfacet_rb/output/json_formatter"
100103
require_relative "asrfacet_rb/output/sarif_formatter"
101104
require_relative "asrfacet_rb/output/jsonl_stream"
102105
require_relative "asrfacet_rb/output/txt_formatter"
103106
require_relative "asrfacet_rb/output/html_formatter"
104107
require_relative "asrfacet_rb/output/change_tracker"
108+
require_relative "asrfacet_rb/output/ruby/txt_renderer"
109+
require_relative "asrfacet_rb/output/ruby/html_renderer"
110+
require_relative "asrfacet_rb/output/ruby/json_renderer"
111+
require_relative "asrfacet_rb/output/ruby/csv_renderer"
112+
require_relative "asrfacet_rb/output/ruby/pdf_renderer"
113+
require_relative "asrfacet_rb/output/ruby/docx_renderer"
114+
require_relative "asrfacet_rb/output/js/js_pdf_bridge"
115+
require_relative "asrfacet_rb/output/js/js_docx_bridge"
116+
require_relative "asrfacet_rb/output/output_router"
105117
require_relative "asrfacet_rb/renderers/sarif_renderer"
106118
require_relative "asrfacet_rb/graph/exporter"
107119
require_relative "asrfacet_rb/notifiers/webhook_notifier"

lib/asrfacet_rb/key_store.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
# This file is part of ASRFacet-Rb and is subject to the terms
1313
# and conditions defined in the LICENSE file.
1414

15-
require "base64"
1615
require "digest"
1716
require "fileutils"
1817
require "json"
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# frozen_string_literal: true
2+
# SPDX-License-Identifier: Proprietary
3+
#
4+
# ASRFacet-Rb: Attack Surface Reconnaissance Framework
5+
# Copyright (c) 2026 voltsparx
6+
#
7+
# Author: voltsparx
8+
# Repository: https://github.com/voltsparx/ASRFacet-Rb
9+
# Contact: voltsparx@gmail.com
10+
# License: See LICENSE file in the project root
11+
#
12+
# This file is part of ASRFacet-Rb and is subject to the terms
13+
# and conditions defined in the LICENSE file.
14+
15+
require "fileutils"
16+
require "time"
17+
18+
module ASRFacet
19+
module Output
20+
class BaseRenderer
21+
attr_reader :store, :target, :options
22+
23+
def initialize(result_store, target, options = {})
24+
@store = result_store
25+
@target = target
26+
@options = options
27+
end
28+
29+
def render(_output_path)
30+
raise NotImplementedError, "#{self.class}#render must be implemented"
31+
end
32+
33+
protected
34+
35+
def timestamp
36+
Time.now.strftime("%Y-%m-%d %H:%M:%S UTC")
37+
end
38+
39+
def iso_timestamp
40+
Time.now.iso8601
41+
end
42+
43+
def version
44+
ASRFacet::VERSION
45+
end
46+
47+
def report_title
48+
"ASRFacet-Rb Recon Report - #{@target}"
49+
end
50+
51+
def severity_order
52+
{
53+
"critical" => 0,
54+
"high" => 1,
55+
"medium" => 2,
56+
"low" => 3,
57+
"informational" => 4
58+
}
59+
end
60+
61+
def sorted_findings
62+
Array(@store.findings).sort_by do |finding|
63+
severity_order[finding[:severity].to_s.downcase] || 99
64+
end
65+
end
66+
67+
def write!(path, content)
68+
FileUtils.mkdir_p(File.dirname(path))
69+
File.write(path, content, encoding: "UTF-8")
70+
end
71+
72+
def log_success(format, path)
73+
puts "[ok] #{format} report written -> #{path}"
74+
end
75+
76+
def log_error(format, message)
77+
warn "[error] #{format} render failed: #{message}"
78+
end
79+
end
80+
end
81+
end
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# frozen_string_literal: true
2+
# SPDX-License-Identifier: Proprietary
3+
#
4+
# ASRFacet-Rb: Attack Surface Reconnaissance Framework
5+
# Copyright (c) 2026 voltsparx
6+
#
7+
# Author: voltsparx
8+
# Repository: https://github.com/voltsparx/ASRFacet-Rb
9+
# Contact: voltsparx@gmail.com
10+
# License: See LICENSE file in the project root
11+
#
12+
# This file is part of ASRFacet-Rb and is subject to the terms
13+
# and conditions defined in the LICENSE file.
14+
15+
module ASRFacet
16+
module Output
17+
class ChartDataBuilder
18+
def initialize(result_store)
19+
@store = result_store
20+
end
21+
22+
def build
23+
{
24+
severity_distribution: severity_distribution,
25+
subdomain_source_share: subdomain_source_share,
26+
port_frequency: port_frequency,
27+
service_breakdown: service_breakdown,
28+
finding_timeline: finding_timeline,
29+
ip_class_distribution: ip_class_distribution
30+
}
31+
end
32+
33+
def severity_distribution
34+
counts = Hash.new(0)
35+
Array(@store.findings).each do |finding|
36+
severity = finding[:severity]&.to_s&.downcase&.capitalize
37+
severity = "Informational" if severity.empty?
38+
counts[severity] += 1
39+
end
40+
counts.map { |label, value| { label: label, value: value } }
41+
end
42+
43+
def port_frequency
44+
frequency = Hash.new(0)
45+
@store.ports.each_value do |ports|
46+
Array(ports).each { |port| frequency[port[:port].to_s] += 1 }
47+
end
48+
frequency.sort_by { |_port, count| -count }.first(10).map do |port, count|
49+
{ port: port, count: count }
50+
end
51+
end
52+
53+
def service_breakdown
54+
counts = Hash.new(0)
55+
@store.ports.each_value do |ports|
56+
Array(ports).each do |port|
57+
service = port[:service].to_s.downcase
58+
service = "unknown" if service.empty?
59+
counts[service] += 1
60+
end
61+
end
62+
counts.sort_by { |_service, count| -count }.first(8).map do |service, count|
63+
{ label: service, value: count }
64+
end
65+
end
66+
67+
def subdomain_source_share
68+
counts = Hash.new(0)
69+
Array(@store.respond_to?(:subdomains_with_sources) ? @store.subdomains_with_sources : []).each do |entry|
70+
source = entry[:source].to_s.capitalize
71+
source = "Unknown" if source.empty?
72+
counts[source] += 1
73+
end
74+
return [{ label: "All Sources", value: @store.subdomains.size }] if counts.empty?
75+
76+
counts.map { |label, value| { label: label, value: value } }
77+
end
78+
79+
def finding_timeline
80+
grouped = Array(@store.findings).group_by do |finding|
81+
stamp = finding[:found_at]
82+
stamp.respond_to?(:strftime) ? stamp.strftime("%H:%M") : "N/A"
83+
end.transform_values(&:size)
84+
grouped.map { |time, count| { time: time || "N/A", count: count } }.sort_by { |entry| entry[:time] }
85+
end
86+
87+
def ip_class_distribution
88+
counts = { "Private" => 0, "Class A" => 0, "Class B" => 0, "Class C" => 0, "Other" => 0 }
89+
Array(@store.ips).each { |ip| counts[classify_ip(ip)] += 1 }
90+
counts.reject { |_label, value| value.zero? }.map { |label, value| { label: label, value: value } }
91+
end
92+
93+
private
94+
95+
def classify_ip(ip)
96+
octets = ip.to_s.split(".").map(&:to_i)
97+
return "Other" unless octets.size == 4
98+
99+
first = octets[0]
100+
return "Private" if first == 10
101+
return "Private" if first == 172 && octets[1].between?(16, 31)
102+
return "Private" if first == 192 && octets[1] == 168
103+
return "Class A" if first.between?(1, 126)
104+
return "Class B" if first.between?(128, 191)
105+
return "Class C" if first.between?(192, 223)
106+
107+
"Other"
108+
rescue ASRFacet::Error
109+
"Other"
110+
end
111+
end
112+
end
113+
end

lib/asrfacet_rb/output/js/.npmrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
save-exact=true
2+
fund=false
3+
audit=false

0 commit comments

Comments
 (0)