log_sense 1.4.1 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ef1d35932ba8c7fe6e636f6cb507ea99fd6b6026170d62207dfa2606d62bbcb
4
- data.tar.gz: 725dc6e51ace6c9cbd366e3888387ef8f246327c9e1036c66d2eb6351b1f0791
3
+ metadata.gz: '0128717b2ba709bc5dfbb7b755762a757c575ad4307159910cc894aaa3b88f42'
4
+ data.tar.gz: e05054b8eee79a439f5b077e60bc0d95a3e7706c550853333d7d631c458abf91
5
5
  SHA512:
6
- metadata.gz: 592e80cd56f4740cc4003b494c9da960de228025377d630d070a08a495cffac7fd41850d469189510def48f9940700251bd84a141def9b1b15b32fbed967261f
7
- data.tar.gz: 1d802e95fd58c66c4904843478c5ccc8ffc1ce43e87dd199e5b21efc89bcff436b227c646d2e5a386bee22a3d1c67c0341c25fcb4058e48526560d05984f0148
6
+ metadata.gz: 9d9e3dc495f7479292ae96d1bf6298f531258cae74df47ff705aea9613880b0d50aa6a19328f70685484ad1c606bd12a8fb7c87632fe5c9cbefefe4893d9bb4d
7
+ data.tar.gz: b417049bcc119ed82ab4c33d007e15804ba85b2485308ca28448fba512814fc5e8b8b215310bfb5c8108fcdc87292322eefc6826b18718fcbc7fced29eea77cb
data/CHANGELOG.org CHANGED
@@ -2,6 +2,21 @@
2
2
  #+AUTHOR: Adolfo Villafiorita
3
3
  #+STARTUP: showall
4
4
 
5
+ * 1.5.0
6
+
7
+ - [User] Present Unique Visits / day as integer
8
+ - [User] Added Country and Streaks report for rails
9
+ - [User] Changed Streak report in Apache
10
+
11
+ - [Gem] Updated DBIP
12
+ - [Gem] Updated Bundle
13
+
14
+ - [Code] Refactored all reports, so that they are specified
15
+ in the same way
16
+ - [Code] Refactor warning message in textual reports
17
+ - [Code] Build HTML menu for report specification
18
+ - [Code] Various refactoring passes on the code
19
+
5
20
  * 1.4.1
6
21
 
7
22
  - [User] New textual report for Apache
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- log_sense (1.3.1)
4
+ log_sense (1.4.2)
5
5
  browser
6
6
  ipaddr
7
7
  iso_country_codes
@@ -13,9 +13,9 @@ GEM
13
13
  specs:
14
14
  browser (5.3.1)
15
15
  byebug (11.1.3)
16
- ipaddr (1.2.3)
16
+ ipaddr (1.2.4)
17
17
  iso_country_codes (0.7.8)
18
- minitest (5.14.4)
18
+ minitest (5.15.0)
19
19
  rake (12.3.3)
20
20
  sqlite3 (1.4.2)
21
21
  terminal-table (3.0.2)
@@ -32,4 +32,4 @@ DEPENDENCIES
32
32
  rake (~> 12.0)
33
33
 
34
34
  BUNDLED WITH
35
- 2.2.32
35
+ 2.3.3
data/README.org CHANGED
@@ -19,8 +19,6 @@ LogSense reports the following data:
19
19
  - OS, browsers, and devices
20
20
  - IP Country location, thanks to the DPIP lite country DB
21
21
  - Streaks: resources accessed by a given IP over time
22
- - Potential attacks: access to resources which are not meant to be
23
- served by a web server serving static websites
24
22
  - Performance of Rails requests
25
23
 
26
24
  Filters from the command line allow to analyze specific periods and
@@ -33,6 +31,18 @@ And, of course, the compulsory screenshot:
33
31
  #+ATTR_HTML: :width 80%
34
32
  [[file:./apache-screenshot.png]]
35
33
 
34
+
35
+ * An important word of warning
36
+
37
+ [[https://owasp.org/www-community/attacks/Log_Injection][Log poisoning]] is a technique whereby attackers send requests with invalidated
38
+ user input to forge log entries or inject malicious content into the logs.
39
+
40
+ log_sense sanitizes entries of HTML reports, to try and protect from log
41
+ poisoning. *Log entries and URLs in SQLite3, however, are not sanitized*:
42
+ they are stored and read from the log. This is not, in general, an issue,
43
+ unless you use the data from SQLite in environments in which URLs can be
44
+ opened or code executed.
45
+
36
46
  * Motivation
37
47
 
38
48
  LogSense moves along the lines of tools such as [[https://goaccess.io/][GoAccess]] (which
@@ -54,6 +64,7 @@ generated files are then made available on a private area on the web.
54
64
  gem install log_sense
55
65
  #+end_src
56
66
 
67
+
57
68
  * Usage
58
69
 
59
70
  #+begin_src bash :results raw output :wrap example
@@ -73,10 +84,11 @@ generated files are then made available on a private area on the web.
73
84
  -w, --width=WIDTH Maximum width of URL and description columns in text reports
74
85
  -c, --crawlers=POLICY Decide what to do with crawlers (applies to Apache Logs)
75
86
  -n, --no-selfpolls Ignore self poll entries (requests from ::1; applies to Apache Logs)
87
+ --verbose Inform about progress (prints to STDERR)
76
88
  -v, --version Prints version information
77
89
  -h, --help Prints this help
78
90
 
79
- This is version 1.4.1
91
+ This is version 1.5.0
80
92
 
81
93
  Output formats
82
94
  rails parsing can produce the following outputs:
@@ -96,6 +108,7 @@ log_sense -f apache -i access.log -t txt > access-data.txt
96
108
  log_sense -f rails -i production.log -t html -o performance.txt
97
109
  #+end_example
98
110
 
111
+
99
112
  * Change Log
100
113
 
101
114
  See the [[file:CHANGELOG.org][CHANGELOG]] file.
@@ -110,8 +123,8 @@ Concerning the outputs:
110
123
  - HTML reports use [[https://get.foundation/][Zurb Foundation]], [[https://www.datatables.net/][Data Tables]], and [[https://vega.github.io/vega-lite/][Vega Light]], which
111
124
  are all downloaded from a CDN
112
125
  - The textual format is compatible with [[https://orgmode.org/][Org Mode]] and can be further
113
- processed to any format [[https://orgmode.org/][Org Mode]] can be exported to (including HTML
114
- and PDF)
126
+ processed to any format [[https://orgmode.org/][Org Mode]] can be exported to, including HTML
127
+ and PDF, with the word of warning in the section above.
115
128
 
116
129
  * Author and Contributors
117
130
 
@@ -119,8 +132,8 @@ Concerning the outputs:
119
132
 
120
133
  * Known Bugs
121
134
 
122
- No known bugs; an unknown number of unknown bugs.
123
- (See the open issues for the known bugs.)
135
+ No known bugs; an unknown number of unknown bugs. (See the open issues for
136
+ the known bugs.)
124
137
 
125
138
  * License
126
139
 
data/Rakefile CHANGED
@@ -9,7 +9,21 @@ end
9
9
  require_relative './lib/log_sense/ip_locator.rb'
10
10
 
11
11
  desc "Convert Geolocation DB to sqlite"
12
- task :dbip_to_sqlite3, [:filename] do |tasks, args|
13
- filename = args[:filename]
14
- ApacheLogReport::IpLocator::dbip_to_sqlite filename
12
+ task :dbip_to_sqlite3, [:year_month] do |tasks, args|
13
+ filename = "./ip_locations/dbip-country-lite-#{args[:year_month]}.csv"
14
+
15
+ if !File.exist? filename
16
+ puts "Error. Could not find: #{filename}"
17
+ puts
18
+ puts 'I see the following files:'
19
+ puts Dir.glob("ip_locations/dbip-country-lite*").map { |x| "- #{x}\n" }
20
+ puts ''
21
+ puts '1. Download (if necessary) a more recent version from: https://db-ip.com/db/download/ip-to-country-lite'
22
+ puts '2. Save downloaded file to ip_locations/'
23
+ puts '3. Relaunch with YYYY-MM'
24
+
25
+ exit
26
+ else
27
+ LogSense::IpLocator::dbip_to_sqlite filename
28
+ end
15
29
  end
data/exe/log_sense CHANGED
@@ -12,7 +12,7 @@ require 'log_sense.rb'
12
12
  @output_file = @options[:output_file]
13
13
 
14
14
  if ARGV.map { |x| File.exist?(x) }.include?(false)
15
- warn.puts "Error: input file(s) '#{ARGV.reject { |x| File.exist(x) }.join(', ')}' do not exist"
15
+ $stderr.puts "Error: input file(s) '#{ARGV.reject { |x| File.exist(x) }.join(', ')}' do not exist"
16
16
  exit 1
17
17
  end
18
18
  @input_files = ARGV.empty? ? [$stdin] : ARGV.map { |x| File.open(x, 'r') }
@@ -31,35 +31,45 @@ when 'rails'
31
31
  parser_klass = LogSense::RailsLogParser
32
32
  cruncher_klass = LogSense::RailsDataCruncher
33
33
  else
34
- warn.puts "Error: input format #{@options[:input_format]} not understood."
34
+ $stderr.puts "Error: input format #{@options[:input_format]} not understood."
35
35
  exit 1
36
36
  end
37
37
 
38
+ $stderr.puts "Parsing input files..." if @options[:verbose]
38
39
  @db = parser_klass.parse @input_files
39
40
 
40
41
  if @options[:output_format] == 'sqlite'
42
+ $stderr.puts "Saving to SQLite3..." if @options[:verbose]
41
43
  ddb = SQLite3::Database.new(@output_file || 'db.sqlite3')
42
44
  b = SQLite3::Backup.new(ddb, 'main', @db, 'main')
43
45
  b.step(-1) #=> DONE
44
46
  b.finish
45
47
  else
48
+ $stderr.puts "Aggregating data..." if @options[:verbose]
46
49
  @data = cruncher_klass.crunch @db, @options
50
+
51
+ $stderr.puts "Geolocating..." if @options[:verbose]
47
52
  @data = LogSense::IpLocator.geolocate @data
48
53
 
54
+ $stderr.puts "Grouping by country..." if @options[:verbose]
55
+ country_col = @data[:ips][0].size - 1
56
+ @data[:countries] = @data[:ips].group_by { |x| x[country_col] }
57
+
49
58
  @ended_at = Time.now
50
59
  @duration = @ended_at - @started_at
51
60
 
52
61
  @data = @data.merge({
53
62
  command: @command_line,
63
+ filenames: ARGV,
54
64
  log_files: @input_files,
55
65
  started_at: @started_at,
56
66
  ended_at: @ended_at,
57
67
  duration: @duration,
58
68
  width: @options[:width]
59
69
  })
60
-
61
70
  #
62
71
  # Emit Output
63
72
  #
73
+ $stderr.puts "Emitting..." if @options[:verbose]
64
74
  puts LogSense::Emitter.emit @data, @options
65
75
  end
Binary file
@@ -17,15 +17,15 @@ module LogSense
17
17
  @total_days = 0
18
18
  @total_days = (@last_day - @first_day).to_i if @first_day && @last_day
19
19
 
20
- @source_files = db.execute "SELECT distinct(source_file) from LogLine"
20
+ @source_files = db.execute 'SELECT distinct(source_file) from LogLine'
21
21
 
22
- @log_size = db.execute "SELECT count(datetime) from LogLine"
22
+ @log_size = db.execute 'SELECT count(datetime) from LogLine'
23
23
  @log_size = @log_size[0][0]
24
24
 
25
25
  @selfpolls_size = db.execute "SELECT count(datetime) from LogLine where ip == '::1'"
26
26
  @selfpolls_size = @selfpolls_size[0][0]
27
27
 
28
- @crawlers_size = db.execute "SELECT count(datetime) from LogLine where bot == 1"
28
+ @crawlers_size = db.execute 'SELECT count(datetime) from LogLine where bot == 1'
29
29
  @crawlers_size = @crawlers_size[0][0]
30
30
 
31
31
  @first_day_requested = options[:from_date]
@@ -35,7 +35,7 @@ module LogSense
35
35
  @last_day_in_analysis = date_intersect options[:to_date], @last_day, :min
36
36
 
37
37
  @total_days_in_analysis = 0
38
- if @first_day_in_analysis and @last_day_in_analysis
38
+ if @first_day_in_analysis && @last_day_in_analysis
39
39
  @total_days_in_analysis = (@last_day_in_analysis - @first_day_in_analysis).to_i
40
40
  end
41
41
 
@@ -45,24 +45,24 @@ module LogSense
45
45
  filter = [
46
46
  (options[:from_date] ? "date(datetime) >= '#{options[:from_date]}'" : nil),
47
47
  (options[:to_date] ? "date(datetime) <= '#{options[:to_date]}'" : nil),
48
- (options[:only_crawlers] ? "bot == 1" : nil),
49
- (options[:ignore_crawlers] ? "bot == 0" : nil),
48
+ (options[:only_crawlers] ? 'bot == 1' : nil),
49
+ (options[:ignore_crawlers] ? 'bot == 0' : nil),
50
50
  (options[:no_selfpolls] ? "ip != '::1'" : nil),
51
- "true"
51
+ 'true'
52
52
  ].compact.join " and "
53
53
 
54
54
  mega = 1024 * 1024
55
55
  giga = mega * 1024
56
56
  tera = giga * 1024
57
-
57
+
58
58
  # in alternative to sum(size)
59
59
  human_readable_size = <<-EOS
60
- CASE
60
+ CASE
61
61
  WHEN sum(size) < 1024 THEN sum(size) || ' B'
62
62
  WHEN sum(size) >= 1024 AND sum(size) < (#{mega}) THEN ROUND((CAST(sum(size) AS REAL) / 1024), 2) || ' KB'
63
63
  WHEN sum(size) >= (#{mega}) AND sum(size) < (#{giga}) THEN ROUND((CAST(sum(size) AS REAL) / (#{mega})), 2) || ' MB'
64
64
  WHEN sum(size) >= (#{giga}) AND sum(size) < (#{tera}) THEN ROUND((CAST(sum(size) AS REAL) / (#{giga})), 2) || ' GB'
65
- WHEN sum(size) >= (#{tera}) THEN ROUND((CAST(sum(size) AS REAL) / (#{tera})), 2) || ' TB'
65
+ WHEN sum(size) >= (#{tera}) THEN ROUND((CAST(sum(size) AS REAL) / (#{tera})), 2) || ' TB'
66
66
  END AS size
67
67
  EOS
68
68
 
@@ -117,20 +117,19 @@ module LogSense
117
117
 
118
118
  @ips = db.execute "SELECT ip, count(ip), count(distinct(unique_visitor)), #{human_readable_size} from LogLine where #{filter} group by ip order by count(ip) desc limit #{options[:limit]}"
119
119
 
120
- @streaks = db.execute "SELECT ip, substr(datetime, 1, 10), path from LogLine order by ip, datetime"
120
+ @streaks = db.execute 'SELECT ip, substr(datetime, 1, 10), path from LogLine order by ip, datetime'
121
121
  data = {}
122
122
 
123
- self.instance_variables.each do |variable|
124
- var_as_symbol = variable.to_s[1..-1].to_sym
125
- data[var_as_symbol] = eval(variable.to_s)
123
+ instance_variables.each do |variable|
124
+ var_as_symbol = variable.to_s[1..].to_sym
125
+ data[var_as_symbol] = instance_variable_get(variable)
126
126
  end
127
+
127
128
  data
128
129
  end
129
130
 
130
- private
131
-
132
- def self.date_intersect date1, date2, method
133
- if date1 and date2
131
+ def self.date_intersect(date1, date2, method)
132
+ if date1 && date2
134
133
  [date1, date2].send(method)
135
134
  elsif date1
136
135
  date1
@@ -140,4 +139,3 @@ module LogSense
140
139
  end
141
140
  end
142
141
  end
143
-
@@ -44,7 +44,7 @@ module LogSense
44
44
 
45
45
  attr_reader :format
46
46
 
47
- def initialize
47
+ def initialize
48
48
  @format = /#{IP} #{IDENT} #{USERID} \[#{TIMESTAMP}\] "(#{METHOD} #{URL} #{PROTOCOL}|-|.+)" #{RETURN_CODE} #{SIZE} "#{REFERER}" "#{USER_AGENT}"/
49
49
  end
50
50
 
@@ -82,7 +82,7 @@ module LogSense
82
82
  line_number
83
83
  )
84
84
  rescue StandardError => e
85
- warn.puts e.message
85
+ $stderr.puts e.message
86
86
  end
87
87
  end
88
88
  end
@@ -1,8 +1,8 @@
1
+ # coding: utf-8
1
2
  require 'terminal-table'
2
3
  require 'json'
3
4
  require 'erb'
4
5
  require 'ostruct'
5
-
6
6
  module LogSense
7
7
  #
8
8
  # Emit Data
@@ -31,9 +31,7 @@ module LogSense
31
31
  end
32
32
  end
33
33
 
34
- private_class_method
35
-
36
- def self.render(template, vars)
34
+ def self.render(template, vars = {})
37
35
  @template = File.join(File.dirname(__FILE__), 'templates', "_#{template}")
38
36
  erb_template = File.read @template
39
37
  ERB.new(erb_template).result(OpenStruct.new(vars).instance_eval { binding })
@@ -42,11 +40,11 @@ module LogSense
42
40
  def self.escape_javascript(string)
43
41
  js_escape_map = {
44
42
  '<' => '&lt;',
45
- '</' => '&lt;\/',
46
- '\\' => '\\\\',
43
+ '</' => '&lt;/',
47
44
  '\r\n' => '\\r\\n',
48
45
  '\n' => '\\n',
49
46
  '\r' => '\\r',
47
+ '\\' => ' \\\\',
50
48
  '"' => ' \\"',
51
49
  "'" => " \\'",
52
50
  '`' => ' \\`',
@@ -73,7 +71,9 @@ module LogSense
73
71
  # - width width to set
74
72
  def self.shorten(data, heading, width)
75
73
  # indexes of columns which have to be shortened
76
- to_shorten = %w[URL Description Path].map { |x| heading.index x }.compact
74
+ keywords = %w[URL Referers Description Path]
75
+ to_shorten = keywords.map { |x| heading.index x }.compact
76
+
77
77
  if width.nil? || to_shorten.empty? || data[0].nil?
78
78
  data
79
79
  else
@@ -96,7 +96,7 @@ module LogSense
96
96
  # column_alignment: specification of column alignments (works for txt reports)
97
97
  # vega_spec: specifications for Vega output
98
98
  # datatable_options: specific options for datatable
99
- def self.apache_report_specification(data)
99
+ def self.apache_report_specification(data = {})
100
100
  [
101
101
  { title: 'Daily Distribution',
102
102
  header: %w[Day DOW Hits Visits Size],
@@ -326,6 +326,19 @@ module LogSense
326
326
  column_alignment: %i[left right right right left],
327
327
  rows: data[:ips]
328
328
  },
329
+ {
330
+ title: 'Countries',
331
+ header: %w[Country Hits Visits IPs],
332
+ column_alignment: %i[left right right left],
333
+ rows: data[:countries]&.map do |k, v|
334
+ [
335
+ k,
336
+ v.map { |x| x[1] }.inject(&:+),
337
+ v.map { |x| x[2] }.inject(&:+),
338
+ v.map { |x| x[0] }.join(' ')
339
+ ]
340
+ end
341
+ },
329
342
  {
330
343
  title: 'Referers',
331
344
  header: %w[Referers Hits Visits Size],
@@ -333,10 +346,27 @@ module LogSense
333
346
  rows: data[:referers],
334
347
  col: 'small-12 cell'
335
348
  },
349
+ {
350
+ title: 'Streaks',
351
+ report: :html,
352
+ header: ['IP', 'Date', 'Total HTML', 'Total Other', 'HTML', 'Other'],
353
+ column_alignment: %i[left left right right left left],
354
+ rows: data[:streaks]&.group_by { |x| [x[0], x[1]] }&.map do |k, v|
355
+ [
356
+ k[0],
357
+ k[1],
358
+ v.map { |x| x[2] }.compact.select { |x| x.match(/\.html?$/) }.size,
359
+ v.map { |x| x[2] }.compact.reject { |x| x.match(/\.html?$/) }.size,
360
+ v.map { |x| x[2] }.compact.select { |x| x.match(/\.html?$/) }.join(' ■ '),
361
+ v.map { |x| x[2] }.compact.reject { |x| x.match(/\.html?$/) }.join(' ■ ')
362
+ ]
363
+ end,
364
+ col: 'small-12 cell'
365
+ }
336
366
  ]
337
367
  end
338
368
 
339
- def self.rails_report_specification(data)
369
+ def self.rails_report_specification(data = {})
340
370
  [
341
371
  {
342
372
  title: "Daily Distribution",
@@ -469,27 +499,54 @@ module LogSense
469
499
  header: %w[Date IP URL Description Log ID],
470
500
  column_alignment: %i[left left left left left],
471
501
  rows: data[:fatal],
472
- col: "small-12 cell"
502
+ col: 'small-12 cell'
473
503
  },
474
504
  {
475
- title: "Internal Server Errors",
505
+ title: 'Internal Server Errors',
476
506
  header: %w[Date Status IP URL Description Log ID],
477
507
  column_alignment: %i[left left left left left left],
478
508
  rows: data[:internal_server_error],
479
- col: "small-12 cell"
509
+ col: 'small-12 cell'
480
510
  },
481
511
  {
482
- title: "Errors",
512
+ title: 'Errors',
483
513
  header: %w[Log ID Context Description Count],
484
514
  column_alignment: %i[left left left left],
485
515
  rows: data[:error],
486
- col: "small-12 cell"
516
+ col: 'small-12 cell'
487
517
  },
488
518
  {
489
- title: "IPs",
519
+ title: 'IPs',
490
520
  header: %w[IPs Hits Country],
491
521
  column_alignment: %i[left right left],
492
522
  rows: data[:ips]
523
+ },
524
+ {
525
+ title: 'Countries',
526
+ header: %w[Country Hits IPs],
527
+ column_alignment: %i[left right left],
528
+ rows: data[:countries]&.map do |k, v|
529
+ [
530
+ k,
531
+ v.map { |x| x[1] }.inject(&:+),
532
+ v.map { |x| x[0] }.join(' ■ ')
533
+ ]
534
+ end
535
+ },
536
+ {
537
+ title: 'Streaks',
538
+ report: :html,
539
+ header: %w[IP Date Total Resources],
540
+ column_alignment: %i[left left right right left left],
541
+ rows: data[:streaks]&.group_by { |x| [x[0], x[1]] }&.map do |k, v|
542
+ [
543
+ k[0],
544
+ k[1],
545
+ v.size,
546
+ v.map { |x| x[2] }.join(' ■ ')
547
+ ]
548
+ end,
549
+ col: 'small-12 cell'
493
550
  }
494
551
  ]
495
552
  end
@@ -4,19 +4,22 @@ require 'ipaddr'
4
4
  require 'iso_country_codes'
5
5
 
6
6
  module LogSense
7
+ #
8
+ # Populate table of IP Locations from dbip-country-lite
9
+ #
7
10
  module IpLocator
8
- DB_FILE = File.join(File.dirname(__FILE__), "..", "..", "ip_locations", "dbip-country-lite.sqlite3")
11
+ DB_FILE = File.join(File.dirname(__FILE__), '..', '..', 'ip_locations', 'dbip-country-lite.sqlite3')
9
12
 
10
- def self.dbip_to_sqlite db_location
11
- db = SQLite3::Database.new ":memory:"
12
- db.execute "CREATE TABLE ip_location (
13
+ def self.dbip_to_sqlite(db_location)
14
+ db = SQLite3::Database.new ':memory:'
15
+ db.execute 'CREATE TABLE ip_location (
13
16
  from_ip_n INTEGER,
14
17
  from_ip TEXT,
15
18
  to_ip TEXT,
16
19
  country_code TEXT
17
- )"
20
+ )'
18
21
 
19
- ins = db.prepare "INSERT INTO ip_location(from_ip_n, from_ip, to_ip, country_code) values (?, ?, ?, ?)"
22
+ ins = db.prepare 'INSERT INTO ip_location(from_ip_n, from_ip, to_ip, country_code) values (?, ?, ?, ?)'
20
23
  CSV.foreach(db_location) do |row|
21
24
  ip = IPAddr.new row[0]
22
25
  ins.execute(ip.to_i, row[0], row[1], row[2])
@@ -33,29 +36,33 @@ module LogSense
33
36
  SQLite3::Database.new DB_FILE
34
37
  end
35
38
 
36
- def self.locate_ip ip, db
37
- return if not ip
39
+ def self.locate_ip(ip, db)
40
+ return unless ip
38
41
 
39
- ip_n = IPAddr.new(ip).to_i
40
- res = db.execute "SELECT * FROM ip_location where from_ip_n <= #{ip_n} order by from_ip_n desc limit 1"
42
+ query = db.prepare 'SELECT * FROM ip_location where from_ip_n <= ? order by from_ip_n desc limit 1'
41
43
  begin
42
- IsoCountryCodes.find(res[0][3]).name
43
- rescue
44
- res[0][3]
44
+ ip_n = IPAddr.new(ip).to_i
45
+ result_set = query.execute ip_n
46
+ country_code = result_set.map { |x| x[3] }[0]
47
+ IsoCountryCodes.find(country_code).name
48
+ rescue IPAddr::InvalidAddressError
49
+ 'INVALID IP'
50
+ rescue IsoCountryCodes::UnknownCodeError
51
+ country_code
45
52
  end
46
53
  end
47
54
 
48
55
  #
49
56
  # add country code to data[:ips]
50
57
  #
51
- def self.geolocate data
52
- @location_db = IpLocator::load_db
53
- data[:ips].each do |ip|
54
- country_code = IpLocator::locate_ip ip[0], @location_db
55
- ip << country_code
58
+ def self.geolocate(data)
59
+ @location_db = IpLocator.load_db
60
+
61
+ data[:ips].each do |line|
62
+ country_code = IpLocator.locate_ip line[0], @location_db
63
+ line << country_code
56
64
  end
57
65
  data
58
66
  end
59
-
60
67
  end
61
68
  end
@@ -59,6 +59,10 @@ module LogSense
59
59
  args[:no_selfpoll] = true
60
60
  end
61
61
 
62
+ opts.on('--verbose', 'Inform about progress (prints to STDERR)') do
63
+ args[:verbose] = true
64
+ end
65
+
62
66
  opts.on('-v', '--version', 'Prints version information') do
63
67
  puts "log_sense version #{LogSense::VERSION}"
64
68
  puts 'Copyright (C) 2021 Shair.Tech'
@@ -96,6 +100,7 @@ module LogSense
96
100
  args[:ignore_crawlers] ||= false
97
101
  args[:only_crawlers] ||= false
98
102
  args[:no_selfpoll] ||= false
103
+ args[:verbose] ||= false
99
104
 
100
105
  args
101
106
  end
@@ -104,6 +104,9 @@ module LogSense
104
104
 
105
105
  @ips = db.execute "SELECT ip, count(ip) from Event where #{filter} group by ip order by count(ip) desc limit #{options[:limit]}"
106
106
 
107
+ @streaks = db.execute 'SELECT ip, substr(started_at, 1, 10), url from Event order by ip, started_at'
108
+ data = {}
109
+
107
110
  @performance = db.execute "SELECT distinct(controller), count(controller), printf(\"%.2f\", min(duration_total_ms)), printf(\"%.2f\", avg(duration_total_ms)), printf(\"%.2f\", max(duration_total_ms)) from Event group by controller order by controller"
108
111
 
109
112
  @fatal = db.execute ("SELECT strftime(\"%Y-%m-%d %H:%M\", started_at), ip, url, error.description, event.log_id FROM Event JOIN Error ON event.log_id == error.log_id WHERE exit_status == 'F'") || [[]]
@@ -0,0 +1,21 @@
1
+ <nav>
2
+ <h2>Navigation</h2>
3
+ <ul class="no-bullet">
4
+ <% (["Summary", "Log Structure"] +
5
+ menus +
6
+ ["Command Invocation", "Performance"]).each do |item| %>
7
+ <li class="nav-item">
8
+ <a href="#<%= Emitter::slugify item %>" data-close>
9
+ <%= item %>
10
+ </a>
11
+ </li>
12
+ <% end %>
13
+ </ul>
14
+
15
+ <p>
16
+ Generated by
17
+ <a href="https://github.com/avillafiorita/log_sense">LogSense</a> <br />
18
+ on <%= DateTime.now.strftime("%Y-%m-%d %H:%M") %>.<br />
19
+ <a href='https://db-ip.com'>IP Geolocation by DB-IP</a>
20
+ </p>
21
+ </nav>
@@ -6,8 +6,9 @@ shortened = Emitter::shorten(report[:rows], report[:header], data[:width])
6
6
  table = Terminal::Table.new headings: report[:header], rows: shortened
7
7
  table.style = { border_i: "|" }
8
8
  columns = report[:header].size - 1
9
- (0..columns).map { |i| table.align_column(i, report[:column_alignment][i] || :left) }
10
-
9
+ (0..columns).map do |i|
10
+ table.align_column(i, report[:column_alignment][i] || :left)
11
+ end
11
12
  # return it
12
13
  table
13
14
  %>
@@ -18,7 +18,7 @@
18
18
  </li>
19
19
  <li class="stats-list-negative">
20
20
  <% days = data[:last_day_in_analysis] - data[:first_day_in_analysis] %>
21
- <%= days > 0 ? "%.2f" % (data[:total_unique_visits] / days.to_f) : "N/A" %>
21
+ <%= days > 0 ? "%d" % (data[:total_unique_visits] / days) : "N/A" %>
22
22
  <span class="stats-list-label">Unique Visits / Day</span>
23
23
  </li>
24
24
  <li class="stats-list-negative">
@@ -5,7 +5,7 @@ table = Terminal::Table.new rows: [
5
5
  ["Days", data[:total_days_in_analysis]],
6
6
  ["Hits", data[:total_hits]],
7
7
  ["Unique Visits", data[:total_unique_visits]],
8
- ["Unique Visits / Day", data[:total_days_in_analysis] > 0 ? "%.2f" % (data[:total_unique_visits] / data[:total_days_in_analysis].to_f) : "N/A"],
8
+ ["Unique Visits / Day", data[:total_days_in_analysis] > 0 ? "%d" % (data[:total_unique_visits] / data[:total_days_in_analysis]) : "N/A"],
9
9
  ["Hits/Unique Visitor", data[:total_unique_visits] != 0 ? data[:total_hits] / data[:total_unique_visits] : "N/A"]
10
10
  ]
11
11
  table.style = { border_i: "|" }
@@ -0,0 +1 @@
1
+ >>>> URLs IN THIS FILE ARE NOT SANITIZED AND MIGHT BE UNSAFE TO OPEN <<<<
@@ -1,7 +1,9 @@
1
1
  <!doctype html>
2
2
  <html class="no-js" lang="en">
3
3
  <head>
4
- <title><%= options[:title] || "Log Sense: #{data[:log_file]}" %></title>
4
+ <title>
5
+ <%= options[:title] || "Log Sense: #{data[:filenames].empty? ? "stdin" : data[:filenames].join(", ")}" %>
6
+ </title>
5
7
 
6
8
  <meta charset="utf-8" />
7
9
  <meta http-equiv="x-ua-compatible" content="ie=edge">
@@ -150,42 +152,11 @@
150
152
  <body>
151
153
  <div class="off-canvas-wrapper">
152
154
  <div class="off-canvas position-left" id="offCanvas" data-off-canvas>
153
- <nav>
154
- <h2>Navigation</h2>
155
- <ul class="no-bullet">
156
- <% [ "Summary",
157
- "Log Structure",
158
- "Daily Distribution",
159
- "Time Distribution",
160
- "20_ and 30_ on HTML pages",
161
- "20_ and 30_ on other resources",
162
- "40_ and 50_ on HTML pages",
163
- "40_ and 50_ on other Resources",
164
- "Statuses",
165
- "Daily Statuses",
166
- "Browsers",
167
- "Platforms",
168
- "Referers",
169
- "IPs",
170
- "Geolocation",
171
- "Streaks",
172
- "Command Invocation",
173
- "Performance"
174
- ].each do |item| %>
175
- <li class="nav-item">
176
- <a href="#<%= item.downcase.gsub(' ', '-') %>" data-close><%= item %></a>
177
- </li>
178
- <% end %>
179
- </ul>
180
-
181
- <p>
182
- Generated by
183
- <a href="https://github.com/avillafiorita/log_sense">LogSense</a> <br />
184
- on <%= DateTime.now.strftime("%Y-%m-%d %H:%M") %>.<br />
185
- <a href='https://db-ip.com'>IP Geolocation by DB-IP</a>
186
- </p>
187
- </nav>
155
+ <%= render "navigation.html.erb",
156
+ menus: Emitter::apache_report_specification.map { |x| x[:title] } +
157
+ ["Streaks", "Command Invocation", "Performance"] %>
188
158
  </div>
159
+
189
160
  <div class="off-canvas-content grid-container grid-x fluid" data-off-canvas-content>
190
161
  <div data-sticky-container>
191
162
  <div class="sticky" data-sticky data-margin-top="0">
@@ -196,14 +167,14 @@
196
167
  </div>
197
168
 
198
169
  <section class="main-section">
199
- <h1><%= options[:title] || "Log Sense: #{data[:log_file]}" %></h1>
170
+ <h1><%= options[:title] || "Log Sense Apache Log Report" %></h1>
200
171
 
201
- <p><b>Input File:</b> <%= (data[:log_file] || "stdin") %></p>
172
+ <p><b>Input File(s):</b> <%= data[:filenames].empty? ? "stdin" : data[:filenames].join(", ") %></p>
202
173
 
203
174
  <div class="grid-x grid-margin-x">
204
175
  <article class="card small-12 large-6 cell">
205
176
  <div class="card-divider">
206
- <h2 id="summary">Summary</h2>
177
+ <h2 id="<%= Emitter::slugify "Summary" %>">Summary</h2>
207
178
  </div>
208
179
  <div class="card-section">
209
180
  <%= render "summary.html.erb", data: data %>
@@ -212,7 +183,7 @@
212
183
 
213
184
  <article class="card cell small-12 large-6">
214
185
  <div class="card-divider">
215
- <h2 id="log-structure">Log Structure</h2>
186
+ <h2 id="<%= Emitter::slugify "Summary" %>">Log Structure</h2>
216
187
  </div>
217
188
  <div class="card-section">
218
189
  <%= render "log_structure.html.erb", data: data %>
@@ -224,7 +195,7 @@
224
195
  <% @reports.each_with_index do |report, index| %>
225
196
  <article class="card cell <%= report[:col] || "small-12 large-6" %>" >
226
197
  <div class="card-divider">
227
- <h2 id="<%= report[:title].downcase.gsub(' ', '-') %>">
198
+ <h2 id="<%= Emitter::slugify report[:title] %>">
228
199
  <%= report[:title] %>
229
200
  </h2>
230
201
  </div>
@@ -252,101 +223,10 @@
252
223
  <% end %>
253
224
  </div>
254
225
 
255
- <article class="card">
256
- <div class="card-divider">
257
- <h2 id="geolocation">Geolocation</h2>
258
- </div>
259
- <div class="card-section">
260
- <table id="geolocation-table" class="table unstriped">
261
- <thead>
262
- <tr>
263
- <th>Country Code</th>
264
- <th>Total Hits</th>
265
- <th>Total Visits</th>
266
- <th>IPs</th>
267
- </tr>
268
- </thead>
269
- <tbody>
270
- <%# IP, Hits, Visits Size, Country%>
271
- <% data[:ips].group_by { |x| x[4] }.each do |k, v| %>
272
- <tr>
273
- <td class="country"><%= k %></td>
274
- <td class="total-hits"><%= v.map { |x| x[1] }.inject(&:+) %></td>
275
- <td class="total-visits"><%= v.map { |x| x[2] }.inject(&:+) %></td>
276
- <td class="ips">
277
- <%= v.map { |x| "<a href=\"https://whatismyipaddress.com/ip/#{x[0]}\">#{x[0]}</a>" }.join(", ") %>
278
- </td>
279
- </tr>
280
- <% end %>
281
- </tbody>
282
- </table>
283
- </div>
284
- </article>
285
-
286
- <article class="card">
287
- <div class="card-divider">
288
- <h2 id="streaks">Streaks</h2>
289
- </div>
290
- <div class="card-section">
291
- <table id="streaks-table" class="table data-table streaks">
292
- <thead>
293
- <tr>
294
- <th>IP</th>
295
- <th>
296
- <div class="grid-x grid-margin-x">
297
- <div class="small-2 cell">
298
- Day
299
- </div>
300
- <div class="small-10 cell">
301
- Resources
302
- </div>
303
- </div>
304
- </th>
305
- </tr>
306
- </thead>
307
- <tbody>
308
- <% data[:streaks].group_by(&:first).each do |ip, date_urls| %>
309
- <tr>
310
- <td class="ip">
311
- <a href="https://whatismyipaddress.com/ip/<%= ip %>"><%= ip %></a>
312
- </td>
313
- <td class="streaks">
314
- <div class="grid-x grid-margin-x">
315
- <% date_urls.group_by { |x| x[1] }.each do |date, urls| %>
316
- <div class="small-12 medium-1 cell">
317
- <span class="date"><%= date %></span>
318
- </div>
319
- <div class="small-12 medium-5 cell">
320
- <span class="res-title">HTML:</span>
321
- <% unique_with_count = urls.map { |x| x[2] }.compact.group_by{|e| e}.map{|k, v| [k, v.length]} %>
322
- <ul class="no-bullet">
323
- <% unique_with_count.select { |x| x[0].match /.*\.html?/ }.each do |url| %>
324
- <li>[<%= url[1] %>] <%= Emitter::escape_javascript url[0] %></li>
325
- <% end %>
326
- </ul>
327
- </div>
328
- <div class=" small-12 medium-5 cell">
329
- <span class="res-title">Other Resources:</span>
330
- <ul class="no-bullet">
331
- <% unique_with_count.select { |x| x[0] and ! x[0].match /.*\.html?/ }.each do |url| %>
332
- <li>[<%= url[1] %>] <%= Emitter::escape_javascript url[0] %></li>
333
- <% end %>
334
- </ul>
335
- </div>
336
- <% end %>
337
- </div>
338
- </td>
339
- </tr>
340
- <% end %>
341
- </tbody>
342
- </table>
343
- </div>
344
- </article>
345
-
346
226
  <div class="grid-x grid-margin-x">
347
227
  <div class="cell small-12 large-6">
348
228
  <article>
349
- <h2 id="command-invocation">Command Invocation</h2>
229
+ <h2 id="<%= Emitter::slugify "Command Invocation" %>">Command Invocation</h2>
350
230
 
351
231
  <%= render "command_invocation.html.erb", data: data, options: options %>
352
232
  </article>
@@ -354,7 +234,7 @@
354
234
 
355
235
  <div class="small-12 large-6 cell">
356
236
  <article>
357
- <h2 id="performance">Performance</h2>
237
+ <h2 id="<%= Emitter::slugify "Performance" %>">Performance</h2>
358
238
 
359
239
  <%= render "performance.html.erb", data: data %>
360
240
  </article>
@@ -1,30 +1,17 @@
1
1
  * Apache Log Analysis
2
2
 
3
- >>>> URLs NOT SANITIZED. DO NOT CONVERT TO HTML <<<<
4
- >>>> (USE THE HTML EXPORT FUNCTION INSTEAD) <<<<
3
+ <%= render "warning.txt.erb" %>
5
4
 
6
5
  ** Summary
7
6
 
8
7
  <%= render "summary.txt.erb", data: data %>
9
8
 
10
- <% @reports.each do |report| %>
9
+ <% @reports.reject { |x| x[:report] == :html }.each do |report| %>
11
10
  ** <%= report[:title] %>
12
11
 
13
12
  <%= render "output_table.txt.erb", report: report, data: data %>
14
13
  <% end %>
15
14
 
16
- ** Geolocation
17
-
18
- <%=
19
- ips = data[:ips].group_by { |x| x[4] }.map { |k, v|
20
- [k, v.map { |x| x[1] }.inject(&:+), v.map { |x| x[2] }.inject(&:+) ]
21
- }
22
- table = Terminal::Table.new headings: ["Country", "Hits", "Total Visits"], rows: ips
23
- table.style = { border_i: "|" }
24
- (1..2).map { |i| table.align_column(i, :right) }
25
- table
26
- %>
27
-
28
15
  ** Command Invocation
29
16
 
30
17
  <%= render 'command_invocation.txt.erb', data: data %>
@@ -1,7 +1,9 @@
1
1
  <!doctype html>
2
2
  <html class="no-js" lang="en">
3
3
  <head>
4
- <title><%= options[:title] || "Log Sense: #{data[:log_file]}" %></title>
4
+ <title>
5
+ <%= options[:title] || "Log Sense: #{data[:filenames].empty? ? "stdin" : data[:filenames].join(", ")}" %>
6
+ </title>
5
7
 
6
8
  <meta charset="utf-8" />
7
9
  <meta http-equiv="x-ua-compatible" content="ie=edge">
@@ -150,36 +152,8 @@
150
152
  <body>
151
153
  <div class="off-canvas-wrapper">
152
154
  <div class="off-canvas position-left" id="offCanvas" data-off-canvas>
153
- <nav>
154
- <h2>Navigation</h2>
155
- <ul class="no-bullet">
156
- <% [
157
- "Summary",
158
- "Log Structure",
159
- "Daily Distribution",
160
- "Time Distribution",
161
- "Statuses",
162
- "Rails Performance",
163
- "Fatal Events",
164
- "Internal Server Errrors",
165
- "Errors",
166
- "IPs",
167
- "Command Invocation",
168
- "Performance"
169
- ].each do |item| %>
170
- <li class="nav-item">
171
- <a href="#<%= item.downcase.gsub(' ', '-') %>" data-close><%= item %></a>
172
- </li>
173
- <% end %>
174
- </ul>
175
-
176
- <p>
177
- Generated by
178
- <a href="https://github.com/avillafiorita/log_sense">LogSense</a> <br />
179
- on <%= DateTime.now.strftime("%Y-%m-%d %H:%M") %>.<br />
180
- <a href='https://db-ip.com'>IP Geolocation by DB-IP</a>
181
- </p>
182
- </nav>
155
+ <%= render "navigation.html.erb",
156
+ menus: Emitter::rails_report_specification.map { |x| x[:title] } %>
183
157
  </div>
184
158
  <div class="off-canvas-content grid-container grid-x fluid" data-off-canvas-content>
185
159
  <div data-sticky-container>
@@ -191,14 +165,14 @@
191
165
  </div>
192
166
 
193
167
  <section class="main-section">
194
- <h1><%= options[:title] || "Log Sense: #{data[:log_file]}" %></h1>
168
+ <h1><%= options[:title] || "Log Sense Rails Log Report" %></h1>
195
169
 
196
- <p><b>Input File:</b> <%= (data[:log_file] || "stdin") %></p>
170
+ <p><b>Input File(s):</b> <%= data[:filenames].empty? ? "stdin" : data[:filenames].join(", ") %></p>
197
171
 
198
172
  <div class="grid-x grid-margin-x">
199
173
  <article class="card small-12 large-6 cell">
200
174
  <div class="card-divider">
201
- <h2 id="summary">Summary</h2>
175
+ <h2 id="<%= Emitter::slugify "Summary" %>">Summary</h2>
202
176
  </div>
203
177
  <div class="card-section">
204
178
  <%= render "summary.html.erb", data: data %>
@@ -207,7 +181,7 @@
207
181
 
208
182
  <article class="card cell small-12 large-6">
209
183
  <div class="card-divider">
210
- <h2 id="log-structure">Log Structure</h2>
184
+ <h2 id="<%= Emitter::slugify "Log Structure" %>">Log Structure</h2>
211
185
  </div>
212
186
  <div class="card-section">
213
187
  <%= render "log_structure.html.erb", data: data %>
@@ -249,7 +223,9 @@
249
223
  <div class="grid-x grid-margin-x">
250
224
  <div class="cell small-12 large-6">
251
225
  <article>
252
- <h2 id="command-invocation">Command Invocation</h2>
226
+ <h2 id="<%= Emitter::slugify "Command Invocation" %>">
227
+ Command Invocation
228
+ </h2>
253
229
 
254
230
  <%= render "command_invocation.html.erb", data: data, options: options %>
255
231
  </article>
@@ -257,7 +233,7 @@
257
233
 
258
234
  <div class="small-12 large-6 cell">
259
235
  <article>
260
- <h2 id="performance"> Performance</h2>
236
+ <h2 id="<%= Emitter::slugify "Performance" %>"> Performance</h2>
261
237
 
262
238
  <%= render "performance.html.erb", data: data %>
263
239
  </article>
@@ -1,13 +1,12 @@
1
1
  * Rails Log Analysis
2
2
 
3
- >>>> URLs NOT SANITIZED. DO NOT CONVERT TO HTML <<<<
4
- >>>> (USE THE HTML EXPORT FUNCTION INSTEAD) <<<<
3
+ <%= render "warning.txt.erb" %>
5
4
 
6
5
  ** Summary
7
6
 
8
7
  <%= render "summary.txt.erb", data: data %>
9
8
 
10
- <% @reports.each do |report| %>
9
+ <% @reports.reject { |x| x[:report] == :html }.each do |report| %>
11
10
  ** <%= report[:title] %>
12
11
 
13
12
  <%= render "output_table.txt.erb", report: report, data: data %>
@@ -15,9 +14,9 @@
15
14
 
16
15
  ** Command Invocation
17
16
 
18
- <%= render "command_invocation.txt.erb", data: data %>
17
+ <%= render 'command_invocation.txt.erb', data: data %>
19
18
 
20
- ** Log Sense Performance
19
+ ** Performance
21
20
 
22
- <%= render "performance.txt.erb", data: data %>
21
+ <%= render 'performance.txt.erb', data: data %>
23
22
 
@@ -1,3 +1,3 @@
1
1
  module LogSense
2
- VERSION = "1.4.1"
2
+ VERSION = "1.5.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: log_sense
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adolfo Fibrillation
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-03-07 00:00:00.000000000 Z
11
+ date: 2022-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: browser
@@ -141,6 +141,7 @@ files:
141
141
  - lib/log_sense/templates/_command_invocation.html.erb
142
142
  - lib/log_sense/templates/_command_invocation.txt.erb
143
143
  - lib/log_sense/templates/_log_structure.html.erb
144
+ - lib/log_sense/templates/_navigation.html.erb
144
145
  - lib/log_sense/templates/_output_table.html.erb
145
146
  - lib/log_sense/templates/_output_table.txt.erb
146
147
  - lib/log_sense/templates/_performance.html.erb
@@ -148,6 +149,7 @@ files:
148
149
  - lib/log_sense/templates/_report_data.html.erb
149
150
  - lib/log_sense/templates/_summary.html.erb
150
151
  - lib/log_sense/templates/_summary.txt.erb
152
+ - lib/log_sense/templates/_warning.txt.erb
151
153
  - lib/log_sense/templates/apache.html.erb
152
154
  - lib/log_sense/templates/apache.txt.erb
153
155
  - lib/log_sense/templates/rails.html.erb