Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Out of Memory Error on Large Adhoc Report #99

Closed
bk5115545 opened this issue Dec 30, 2014 · 11 comments
Closed

Out of Memory Error on Large Adhoc Report #99

bk5115545 opened this issue Dec 30, 2014 · 11 comments

Comments

@bk5115545
Copy link

I have several huge reports to download and I have been having the console generate them after every scan. This approach works but I have to have enough disk space for the console to store them. My problem is that I don't have this much disk space to work with.

My solution is to switch from the console generating the report and then me downloading it to using the new Connection::past_scans method and Nexpose::AdhocReportConfig. I run my code in a cronjob that generates and downloads scans since it's last run. Seems simple enough.

Minimal code snippit is below.

nsc = Nexpose::Connection.new(...)
nsc.login
most_recent = nil
scans = nsc.past_scans(expected*4)
scans.reject! { |scan| scan.end_time.to_i < most_recent.end_time.to_i  || scan.id == most_recent.id } if not most_recent.nil?
#### ... code to change #{expected} for next run... ####
nsc.logout
scans.peach(2) do |scan| #download 2 reports at a time
  local_nexpose = Nexpose::Connection.new(...)
  local_nexpose.login
  config = Nexpose::AdhocReportConfig.new('audit-report', 'raw-xml-v2', nil, nil, 0)
  config.add_filter('scan', scan.id)
  raw_xml = config.generate(local_nexpose, timeout=3600)
  File.write("unique_name.xml", raw_data)
  most_recent = scan
  local_nexpose.logout
end

Commit 7903b8d was a great step since I can now download 200MB reports however I still crash on reports larger than 350MB. I have looked at making the reports smaller however I need every report to contain exactly 1 whole network site. Some of these sites have about 1000 RHSA updates that need to be applied and each of these updates contains way too much vulnerability information.

Stacktrace is below.

Downloading scan 412687 for <SITENAME_THATS_NOT_PUBLIC>.
/home/<username>/.rvm/gems/ruby-2.1.5/gems/nexpose-0.8.19/lib/nexpose/api_request.rb:128:in `execute': NexposeAPI: Action failed: Error parsing response: #<RegexpError: failed to allocate memory: /\A([^<]*)/m> (Nexpose::APIError)
/home/<username>/.rvm/rubies/ruby-2.1.5/lib/ruby/2.1.0/rexml/source.rb:219:in `match'
/home/<username>/.rvm/rubies/ruby-2.1.5/lib/ruby/2.1.0/rexml/source.rb:219:in `match'
/home/<username>/.rvm/rubies/ruby-2.1.5/lib/ruby/2.1.0/rexml/parsers/baseparser.rb:426:in `pull_event'
/home/<username>/.rvm/rubies/ruby-2.1.5/lib/ruby/2.1.0/rexml/parsers/baseparser.rb:184:in `pull'
/home/<username>/.rvm/rubies/ruby-2.1.5/lib/ruby/2.1.0/rexml/parsers/treeparser.rb:22:in `parse'
/home/<username>/.rvm/rubies/ruby-2.1.5/lib/ruby/2.1.0/rexml/document.rb:287:in `build'
/home/<username>/.rvm/rubies/ruby-2.1.5/lib/ruby/2.1.0/rexml/document.rb:44:in `initialize'
/home/<username>/.rvm/gems/ruby-2.1.5/gems/nexpose-0.8.19/lib/nexpose/util.rb:12:in `new'
/home/<username>/.rvm/gems/ruby-2.1.5/gems/nexpose-0.8.19/lib/nexpose/util.rb:12:in `parse_xml'
/home/<username>/.rvm/gems/ruby-2.1.5/gems/nexpose-0.8.19/lib/nexpose/api_request.rb:50:in `execute'
/home/<username>/.rvm/gems/ruby-2.1.5/gems/nexpose-0.8.19/lib/nexpose/api_request.rb:127:in `execute'
/home/<username>/.rvm/gems/ruby-2.1.5/gems/nexpose-0.8.19/lib/nexpose/connection.rb:90:in `execute'
/home/<username>/.rvm/gems/ruby-2.1.5/gems/nexpose-0.8.19/lib/nexpose/report.rb:238:in `generate'
test.rb:71:in `block in <main>'
/home/<username>/.rvm/gems/ruby-2.1.5/gems/peach-0.5.1/lib/peach.rb:22:in `block (2 levels) in peach'
/home/<username>/.rvm/gems/ruby-2.1.5/gems/peach-0.5.1/lib/peach.rb:22:in `each'
/home/<username>/.rvm/gems/ruby-2.1.5/gems/peach-0.5.1/lib/peach.rb:22:in `block in peach'
/home/<username>/.rvm/gems/ruby-2.1.5/gems/peach-0.5.1/lib/peach.rb:13:in `block (2 levels) in _peach_run'
...
Exception parsing
Line: 1282579
Position: 100040694
Last 80 unconsumed characters:
 Content-Transfer-t/xml; name=report.xml
        from /home/<username>/.rvm/gems/ruby-2.1.5/gems/nexpose-0.8.19/lib/nexpose/connection.rb:90:in `execute'
        from /home/<username>/.rvm/gems/ruby-2.1.5/gems/nexpose-0.8.19/lib/nexpose/report.rb:238:in `generate'
        from test.rb:71:in `block in <main>'

According to the stacktrace, the issue is actually in REXML however REXML developers already closed the issue with a status of "WILL NOT FIX" saying that it's a problem with Base64 encoding and XML.

As far as I can tell, the only reason the Nexpose gem is parsing the XML is to check the value of the success attribute on the root node. There is probably not a reason to have the gem parse the entire report if the report is larger than 32MB. I have a monkey patch that drops the header and footer and uses the Base64 module to get me the raw xml. This patch is really slow though since it causes the generic errors in APIRequest::execute to trigger retrying 5 times. I would rather help fix the issue upstream than throw 3 or 4 monkey patches into production.

bk5115545 referenced this issue Dec 30, 2014
Code was needlessly parsing XML and HTML reports through REXML and
Nokogiri. It will be faster and less memory intensive to just pass the
string along, since #to_s was called on the resulting parse. If
we need to validate the XML for reports, that should be done in testing.
mdaines-r7 added a commit that referenced this issue Dec 30, 2014
Attempts to address #99
The generate method can now be passed with a "raw" flag to indicate that
the response should not be parsed as XML for exception handing.
Example usage:
```
  config = AdhocReportConfig.new('audit-report', 'raw-xml-v2', site.id)
  report = config.generate(nsc, 3600, true)
```
@mdaines-r7
Copy link
Contributor

@bk5115545 ... see if this addresses the issue with the larger files.

An example usage for grabbing the raw data is:

  config = AdhocReportConfig.new('audit-report', 'raw-xml-v2', site.id)
  report = config.generate(nsc, 3600, true)
  File.write("export-#{site.id}.xml", report)

If that works out, I'll keep it in there. Otherwise, I'll strip it out, as it adds complexity.

@bk5115545
Copy link
Author

It worked on a couple of reports however then this happened.

/home/<username>/.rvm/gems/ruby-2.1.5/gems/nexpose-0.9.0/lib/nexpose/api_request.rb:139:in `execute': NexposeAPI: Action failed: User requested raw XML response. Not parsing failures. (Nexpose::APIError)
        from /home/<username>/.rvm/gems/ruby-2.1.5/gems/nexpose-0.9.0/lib/nexpose/connection.rb:90:in `execute'
        from /home/<username>/.rvm/gems/ruby-2.1.5/gems/nexpose-0.9.0/lib/nexpose/report.rb:240:in `generate'
        from sample.rb:66:in `block in <main>'

Line 66 in sample.rb is the config.generate(...) call

@mdaines-r7
Copy link
Contributor

When that happens, you should be able to inspect the raw xml to see if there is an error in there. That error message should only happen because the response didn't contain the string 'success="1"' ... there may be a legitimate (non-Ruby) error in there.

You can view the raw response by inspecting it on the connection:

local_nexpose.response_xml

If you haven't made any further calls to the nexpose console, that will contain the ad hoc report response.

@bk5115545
Copy link
Author

While I'm waiting on the application to crash again so I can inspect the XML, I was looking through the scan log provided by the web gui. I found the below quite frequently.

2014-12-30T21:36:29 [INFO] [Thread: VulnerabilityCheckContext.performTests] [Site: <SITENAME>] [<target_ip>:443] apache-httpd-cve-2007-6388 (apache-httpd-cve-2007-6388-mod_status-open-redir-exploit13) - ERROR - (Enable verbose logging for more information): 
java.lang.NullPointerException
    at com.rapid7.nexpose.plugin.vulnck.ValueTest.matches(Unknown Source)
    at com.rapid7.nexpose.plugin.http.BaseHTTPCheckHandler$HTTPResponseTest.isHeaderMatch(Unknown Source)
    at com.rapid7.nexpose.plugin.http.BaseHTTPCheckHandler$HTTPResponseTest.isHeadersMatch(Unknown Source)
    at com.rapid7.nexpose.plugin.http.BaseHTTPCheckHandler$HTTPResponseTest.isMatch(Unknown Source)
    at com.rapid7.nexpose.plugin.http.BaseHTTPCheckHandler.getResponseMatch(Unknown Source)
    at com.rapid7.nexpose.plugin.http.HTTPCheckHandler$HTTPReqRespTest.isMatch(Unknown Source)
    at com.rapid7.nexpose.plugin.http.HTTPCheckHandler$HTTPCheckTest.performCheck(Unknown Source)
    at com.rapid7.nexpose.plugin.http.HTTPCheckHandler.handle(Unknown Source)
    at com.rapid7.nexpose.plugin.BaseCheckContext.invokeTest(Unknown Source)
    at com.rapid7.nexpose.nse.VulnerabilityCheckContext.performTests(Unknown Source)
    at com.rapid7.nexpose.nse.VulnerabilityCheckContext.performTests(Unknown Source)
    at sun.reflect.GeneratedMethodAccessor975.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.rapid7.thread.ThreadedCall.invokeCall(Unknown Source)
    at com.rapid7.thread.ThreadedCall.execute(Unknown Source)
    at com.rapid7.thread.ThreadedCallRunner.executeCall(Unknown Source)
    at com.rapid7.thread.ThreadedCallRunner.run(Unknown Source)

I know that it's not related to our changes with the gem but does this mean that the Nexpose console can't generate a report for sites with this error?

I also know that this error could have been caused by my team writing a faulty vuln check but we have never written our own vuln checks.

@mdaines-r7
Copy link
Contributor

It's hard to tell exactly what that stacktrace is. It could be harmless, but we'd have to have more context to really get to the heart of it (i.e., a support case with logs, etc.). It might show up in a report card with the "Unknown" status, if the check fully failed and couldn't continue.

Something in the scan log shouldn't affect what you are seeing in an export report, but there could be something about the data collected.

@bk5115545
Copy link
Author

Alright I have found the actual issue. The above stack trace isn't good but the issue was that there were too many connections to the Nexpose console internal postgres db and the db denied the connection request. It's in the logs on the Nexpose console and the timestamps nearly match. At that time, we had about 40 scans going on 40 different scan engines.

I'll just catch that exception and retry after a long delay.

The fix works great but some other issue got in the way of seeing it.

@mdaines-r7
Copy link
Contributor

OK. And, yeah, Nexpose (and the DB) will throttle activity if there's too much going on to protect itself. If you see something new, just reopen this issue or a new one. Thanks.

@jhart-r7
Copy link
Contributor

As an aside, I am fairly certain that stack trace is hinting at a real, unrelated defect. I've filed it internally to get it tracked down (CONTENT-7079, internally)

@TheCaucasianAsian
Copy link

I just started getting this issue as well. I'm generating hundreds of ad hoc reports, is there a fix or do we just have some sort of timeout in between requests?

@bk5115545
Copy link
Author

@TheCaucasianAsian I tried adding a delay between my requests but it wasn't very effective. All my reports took varying times and a static delay didn't use the resources efficiently. I settled for the peach module, a wrapper around os.Popen to only download a few reports at a time. In my testing, I didn't see any performance improvement if I tried to generate more than 2 reports at a time.

If you use peach, be careful that all operations in the loop are completely independent of both previous iterations and variables outside the loop unless you want to add locking to a mostly single-threaded language. This is why I saved my reports in the loop ("unique_name.xml" was a computed name based on time and the sitename).

@TheCaucasianAsian
Copy link

Damn, right now I'm at the point where I just keep track of all the report names I try to generate, then loop back thru the directory to see which ones failed. I'm surprised this is still an issue. Thank you for the quick response I'll have to try out your suggestions!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants