Kenna Security is now part of Cisco

|Learn more
Contact Us
Talk to an Expert
Request a demo

Risk Meter Reports – Python

Dec 23, 2021
Rick Ehrhart
API Evangelist

Share with Your Network

I started looking at our “Asset Group Reporting” APIs.  After reading their descriptions, I decided to write down some notes about what they’re reporting.

 

API

Units Dates? Structure
Historical Mean Time To Remediate by Risk Level Days No low, medium, high
Historical Risk Meter Scores Risk meter score Yes
Historical Vulnerability Risk Category Counts Vuln counts Yes low, medium, high
Vulnerabilities by Due Date Vuln counts No Days past due
Total Past Due Vulnerabilities by Risk Level Vuln Counts No low, medium, high
Average Days Open by Risk Level Over Time Days Yes low, medium, high
Risk Accepted by Risk Level Over Time Vuln counts Yes low, medium, high
False Positive by Risk Level Over Time Vuln counts Yes low, medium, high
Past Due Vulnerabilities by Risk Level Over Time Vuln counts Yes low, medium, high

As you can see, most of the reports return vulnerability (vuln) counts.  I started playing with these APIs to see if I could come up with a use case and decided that reporting vulnerability counts with risk accepted status would be useful.

First off, what does risk accepted status mean? According to the help article, “Vulnerability Statuses”, risk accepted status means:  “The vulnerability truly represents a risk, but the business has decided not to remediate it for some reason”.  To me, this is acknowledging the risk by hiding it.

Note: A risk meter score is the vulnerability score of an asset group. The terms risk meter and asset group are used interchangeably.

Finding the true risk score

The straight-forward way to find out if your risk score has risk accepted vulnerabilities is to invoke the “List Asset Group” API and compare risk_score and true_risk_score.  True risk score for a given risk meter according to “Reporting on your True Risk Score” is “calculated to include all risk accepted vulnerabilities that would fall into that asset group IF they were not risk accepted”.

The first Python program that will be discussed will be list_risk_score.py.  This program collects the risk meter scores, displays them, and if there is a difference between risk_score and true_risk_score, it adds two asterisks to the risk meter row output. 

The function, get_risk_meter_scores(), invokes the “List Asset Group” API and returns a dictionary of tuples.  The dictionary is indexed by risk meter name and the tuples contain the risk meter ID and scores.

11 # Get risk meter information, and return a dictionary of tuples containing risk meter ID,
 12 # risk meter score, and true risk meter score.
 13 def get_risk_meter_scores(base_url, headers):
 14     risk_meters = {}
 15     list_risk_meters_url = f"{base_url}asset_groups"
 16 
 17     response = requests.get(list_risk_meters_url, headers=headers)
 18     if response.status_code != 200:
 19         print(f"List Risk Meters Error: {response.status_code} with {list_risk_meters_url}")
 20         sys.exit(1)
 21 
 22     resp_json = response.json()
 23     risk_meters_resp = resp_json['asset_groups']
 24 
 25     for risk_meter in risk_meters_resp:
 26         risk_meter_id = risk_meter['id']
 27         risk_meter_score = risk_meter['risk_meter_score']
 28         true_score = risk_meter['true_risk_meter_score']
 29         risk_meters[risk_meter['name']] = (risk_meter_id, risk_meter_score, true_score)
 30 
 31     return risk_meters

Next the program loops through the dictionary comparing risk score with true risk score.  In this loop, the high, medium, and low risk scores are placed in table rows.  If the risk score and the true risk score does not match, it is noted with two asterisks.

 61     # Go through the list of risk meters filling out the output table.
 62     for risk_meter_name in sorted (risk_meters.keys()):
 63         risk_meter_tuple = risk_meters[risk_meter_name]
 64         risk_meter_id = risk_meter_tuple[0]
 65         risk_meter_score = risk_meter_tuple[1]
 66         risk_meter_true_score = risk_meter_tuple[2]
 67         true_score = f"{risk_meter_true_score}   " if risk_meter_true_score == risk_meter_score else f"{risk_meter_true_score} **"
 68 
 69         risk_meter_tbl.add_row([risk_meter_name, risk_meter_id, risk_meter_score, true_score])

Note that the Python PrettyTable library is used to display data in ASCII tables.

After the Risk Scores Table is displayed, the program runs through the dictionary of risk meters again.  This time when the risk score and the true risk score is different, a subprocess, historical_vuln_count.py, is spawned that will display the vulnerability counts for the risk meter.

86         if risk_meter_score != risk_meter_true_score:
87             rtn_code = 0
88             cmd = ["python",
89                    "historical_vuln_count.py",
90                    risk_meter_name,
91                    "today",
92                    str(risk_meter_id),
93                    str(risk_meter_score),
94                    str(risk_meter_true_score)]
95             subprocess.run(cmd)
96             subprocess.CompletedProcess(cmd, rtn_code)


The Python program, historical_vuln_count.py has been implemented so that it can be run stand-alone or as a subprocess.  As a stand-alone program, it displays a year’s worth of vulnerability counts and risk accepted vulnerability counts on a monthly basis for a specified  risk meter.  Here a year’s worth of vulnerability counts are displayed for the risk meter “Laptops”.

Vulnerability Counts

% python historical_vuln_count.py Laptops    



Historical Risk Meter Vulnerability Counts

Laptops (1797)
Vulnerability Counts and Risk Accepted (RA) Vulnerability Counts
+------------+------+---------+--------+-----------+--------+--------+
|    Date    | High | RA High | Medium | RA Medium |  Low   | RA Low |
+------------+------+---------+--------+-----------+--------+--------+
| 2020-12-01 | 239  |    38   | 26843  |    121    | 57054  | 13533  |
| 2021-01-01 | 186  |    46   | 30953  |    148    | 103836 | 13537  |
| 2021-02-01 | 184  |    42   | 30539  |    147    | 218933 | 13403  |
| 2021-03-01 | 1994 |    42   | 71296  |    141    | 300565 | 13367  |
| 2021-04-01 | 201  |    46   | 39485  |    145    | 201543 | 13364  |
| 2021-05-01 | 133  |    38   | 20905  |    121    | 252860 | 13032  |
| 2021-06-01 | 259  |    35   | 14751  |    118    | 266299 | 12821  |
| 2021-07-01 | 1792 |    61   | 22682  |    173    | 231463 | 13166  |
| 2021-08-01 | 5485 |    70   | 98993  |    206    | 259370 | 12998  |
| 2021-09-01 | 6997 |    70   | 76909  |    203    | 286295 | 12631  |
| 2021-10-01 | 8367 |    69   | 88061  |    196    | 430385 | 12239  |
| 2021-11-01 | 6595 |    70   | 81627  |    196    | 515947 | 12044  |
| 2021-12-01 | 2282 |    69   | 29929  |    192    | 190506 | 11788  |
+------------+------+---------+--------+-----------+--------+--------+

The program also has an optional today command line argument which only displays the vulnerability counts for current day.

% python historical_vuln_count.py Laptops today

Historical Risk Meter Vulnerability Counts

Laptops (1797)
Vulnerability Counts and Risk Accepted (RA) Vulnerability Counts
+------------+------+---------+--------+-----------+--------+--------+
|    Date    | High | RA High | Medium | RA Medium |  Low   | RA Low |
+------------+------+---------+--------+-----------+--------+--------+
| 2021-12-06 | 1059 |    69   | 13754  |    192    | 138881 | 11682  |
+------------+------+---------+--------+-----------+--------+--------+

Finally it has a subprocess mode that takes the following command line arguments:

  • Risk meter ID
  • Risk meter score
  • Risk meter true score

This subprocess mode displays the today information with the risk meter scores.

Production Servers  (17025)  [300, 320]
Vulnerability Counts and Risk Accepted (RA) Vulnerability Counts
+------------+------+---------+--------+-----------+-------+--------+
|    Date    | High | RA High | Medium | RA Medium |  Low  | RA Low |
+------------+------+---------+--------+-----------+-------+--------+
| 2021-12-06 | 108  |   120   |  1254  |    298    | 110382006  |
+------------+------+---------+--------+-----------+-------+--------+

Now let’s examine the code.  There are two flags in the code, one_shot and check_risk_meter_name.  The one_shot flag indicates that there is no monthly iteration, just report once.  This is set when the today command line parameter is processed.  The check_risk_meter_name flag is set when the subprocess command line parameters are passed in, because it is assumed that the risk meter name is correct so that we don’t have to search for it.

The start_date determination is done here:

155     # If one_shot True, then process only today, else process starting last year.
156     today = date.today()
157     if one_shot:
158         start_date = today
159     else:
160         month_beginning = today.replace(day=1)
161         start_date = month_beginning.replace(year=today.year - 1)
162     start_date_str = start_date.isoformat()


Next the risk meter name is obtained.  If  historical_vuln_count.py was spawned as a subprocess with the correct command line parameters, then the risk meter name is not checked.  If the risk meter name needs to be checked, get_a_risk_meter() is called which is discussed in “Risk Meter Vulnerabilities”.Today’s date is obtained. If the
today command line parameter is being processed, start_date is assigned to today’s date, else start_date is set to a year ago.  Since the historical report API takes the date as a string, start_date is converted to a string with isoformat() function.

The function process_reports() is called.  It invokes two report APIs, “Historical Vulnerability Risk Category Counts” and “Risk Accepted by Risk Level Over Time”.  These APIs return a dictionary of dates with each entry containing a dictionary of vulnerability counts for “low”, “medium”, and “high” risk scores.  Since these two reports are similar in request and response, a common function, get_historical_report() is called.

50     vuln_count_report = "historical_open_vulnerability_count_by_risk_level"
51     vuln_count_resp = get_historical_report(base_url, headers, id, vuln_count_report, start_date)
52 
53     accepted_risk_report = "risk_accepted_over_time"
54     accepted_risk_resp = get_historical_report(base_url, headers, id, accepted_risk_report, start_date)

The get_historical_report() function code:

36 def get_historical_report(base_url, headers, id, report, start_date):
37     get_report_url = f"{base_url}asset_groups/{id}/report_query/{report}?start_date={start_date}"
38 
39     # Invoke an Asset Group Report API.
40     response = requests.get( get_report_url, headers=headers)
41     if response.status_code != 200:
42         print(f"Asset Group Report API Error: {response.status_code} with {get_report_url}")
43         sys.exit(1)
44 
45     resp_json = response.json()
46     return resp_json

Note that only the start_date is used.  The end_date defaults to the current date. After that, some risk meter ID checks are made to verify that everything is copacetic.

56     # Verify asset group IDs
57     if vuln_count_resp['id'] != accepted_risk_resp['id']:
58         print(f"Reports don't have matching IDs: {vuln_count_report['id']}, {accepted_risk_resp['id']}")
59         sys.exit(1)
60 
61     if vuln_count_resp['id'] != id:
62         print(f"Reports don't matching requested ID: {vuln_count_report['id']}, {id}")
63         sys.exit(1)

Once the ID checks pass, format the output table for each date.  Note that the dates from the two different reports are verified to be the same in line 75.

 73     # Process both vulnerability count and risk accepted vulnerability count data.
 74     for (vc_date, ra_date) in zip(vuln_count_data, risk_accepted_data):
 75         if vc_date != ra_date:
 76             print(f"Dates don't match: {vc_date} : {ra_date}")
 77             sys.exit(1)
 78 
 79         # Add today or the first of the month to the table.
 80         if one_shot or vc_date[-2:] == "01":
 81             vc_high = int(vuln_count_data[vc_date]['high'])
 82             vc_medium = int(vuln_count_data[vc_date]['medium'])
 83             vc_low = int(vuln_count_data[vc_date]['low'])
 84             ar_high = int(risk_accepted_data[ra_date]['high'])
 85             ar_medium = int(risk_accepted_data[ra_date]['medium'])
 86             ar_low = int(risk_accepted_data[ra_date]['low'])
 87 
 88             vuln_count_tbl.add_row([vc_date, vc_high, ar_high, vc_medium, ar_medium, vc_low, ar_low])


All together nowIf today(
one_shot) is specified or if the date is the beginning of the month, then add a row to the display table.  Basically, the reports return all dates and we just display the beginning of each month or the current date.

So let’s take a look at the output of list_risk_scores.py.

+--------------------+--------+------------+-----------------+
| Risk Meter Name    |   ID   | Risk Score | True Risk Score |
+--------------------+--------+------------+-----------------+
| API Gateway        | 158329 |    370     |      370        |
| Workstations       | 237802 |    520     |      440 **     |
| Cloud Agent        | 215217 |    370     |      370        |
| Domain Controllers | 56761  |    330     |      330        |
| SQL Servers        | 197280 |    230     |      260 **     |
+--------------------+--------+------------+-----------------+

Vulnerability Counts for Unequal Risk Scores
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Workstations (237802)  [520, 440]
Vulnerability Counts and Risk Accepted (RA) Vulnerability Counts
+------------+------+---------+--------+-----------+------+--------+
|    Date    | High | RA High | Medium | RA Medium | Low  | RA Low |
+------------+------+---------+--------+-----------+------+--------+
| 2021-12-064   |    0    |  156   |     1     | 1029 |   13   |
+------------+------+---------+--------+-----------+------+--------+

SQL Servers (197280)  [230, 260]
Vulnerability Counts and Risk Accepted (RA) Vulnerability Counts
+------------+------+---------+--------+-----------+------+--------+
|    Date    | High | RA High | Medium | RA Medium |  Low | RA Low |
+------------+------+---------+--------+-----------+------+--------+
| 2021-12-0613  |    26   |  321   |     36    | 59771363  |
+------------+------+---------+--------+-----------+------+--------+

As you can see we have a table of risk meters with their risk score and true risk score, and detailed tables of vulnerability counts if the scores don’t agree.  

Conclusion

This blog shows how to use two Asset Group Report APIs  in conjunction with the List Asset Group API to create a new report.  This new report allows you to better understand your vulnerability risk and where it is.

 

As always this code is in Kenna Security’s GitHub repository.  The files, list_risk_scores.py and historical_vuln_count.py are located in the python/risk_meter directory. A requirements.txt has been created to assist in installing the examples.

 

Next time the code will be in PowerShell.  Happy Holidays!

Rick Ehrhart

API Evangelist

dev@kennasecurity.com

 

Read the Latest Content

Threat Intelligence

18+ Threat Intel Feeds Power Modern Vulnerability Management

You need lots of threat intelligence feeds to cover all of the threat and vulnerability data categories in the world. Learn about the threat intel feeds...
READ MORE
Data Science

Ask Us About Our Data Science

In vulnerability management, data deluge is a recurring problem. Learn what data science is and how it can help your company.
READ MORE
Risk-Based Vulnerability Management

What is Modern Vulnerability Management?

Modern vulnerability management is an orderly, systematic, and data-driven approach to enterprise vulnerability management.
READ MORE
FacebookLinkedInTwitterYouTube

© 2022 Kenna Security. All Rights Reserved. Privacy Policy.