Scenario Overview
At a client, vendor bank account numbers may only be changed if a colleague (other than the person making the change) has approved the modification. As an auditor, you would like to have some assurance and rely on this internal control. However, during an IT audit, it was discovered that users with administrator rights can bypass this authorization step. The auditor therefore wants to verify that, in practice, all IBAN changes are still correctly authorized.
GITC Impact
Although on paper this control appears to work well, in practice, if someone has too many rights, they can circumvent it. Often these controls are logged, so you should be able to see retrospectively who performed which actions. Two important conditions remain: you must be sure that each user logs in only with their own account (authentication) and that the logging cannot be disabled (or deleted).
Why Data Analysis?
It is a strong internal control, and with a short analysis you cover the GITC gap. The analysis is relatively easy to perform and avoids the need for additional sampling.
Objectives
With data analysis we want to:
- Identify all vendors whose IBAN was changed during the selected period.
- Verify that each change is followed by an approval from a different user.
Required Data and Preparations
For this analysis, we will use:
- Creditors log: an export logging every change to vendor master data.
- Python with the pandas package for data manipulation and datetime for date comparisons.
Analysis
The Python script below shows step by step how to perform an IBAN change control. Copy the full script and use it as a template in your own data environment.
import pandas as pd
from datetime import datetime
# 1. Read the log (note: separator is a semicolon)
df = pd.read_csv("dummy_logs_creditors.csv", sep=";", parse_dates=["Date"], dayfirst=True)
# Display a preview
print(df.head())
# 2a. Find all entries where an IBAN change was created
mask_change = df["Change Type"] == "Bank Account"
df_changes = df[mask_change].copy()
# 2b. Find all entries where a change was approved
mask_approve = df["Change Type"] == "Bank Account" # same Change Type, but Action contains 'Approved'
df_approvals = df[df["Action"].str.startswith("Approved")].copy()
# Function to check for each change if there is a later approval
def check_approval(row):
vendor = row["VendorID"]
change_time = row["Date"]
# filter approvals for the same vendor after the change date
later = df_approvals[
(df_approvals["VendorID"] == vendor) &
(df_approvals["Date"] > change_time)
]
if not later.empty and later.iloc[0]["User"] != row["User"]:
return pd.Series({
"Approved": True,
"Approver": later.iloc[0]["User"],
"ApprovalDate": later.iloc[0]["Date"]
})
else:
return pd.Series({
"Approved": False,
"Approver": None,
"ApprovalDate": None
})
# Apply the function to all change events
df_results = df_changes.join(df_changes.apply(check_approval, axis=1))
# Filter unauthorized cases
df_unauthorized = df_results[df_results["Approved"] == False]
# Report
print("Unauthorized IBAN changes:")
print(df_unauthorized[[
"Date", "VendorID", "Vendor", "Old Value", "New Value"
]])
This analysis examines changes that weren't approved and changes that were approved by the same employee after the change. It's important to carefully review these findings, as these changes may not have been processed at all. You can also determine the impact by determining whether any money was actually transferred to an account identified in the findings.