1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.owasp.dependencycheck.data.nvdcve;
19
20 import com.google.common.io.Resources;
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.net.URL;
25 import java.nio.charset.StandardCharsets;
26 import java.sql.PreparedStatement;
27 import java.sql.Connection;
28 import java.sql.Driver;
29 import java.sql.DriverManager;
30 import java.sql.ResultSet;
31 import java.sql.SQLException;
32 import java.sql.Statement;
33 import java.util.Locale;
34 import java.util.ResourceBundle;
35 import javax.annotation.concurrent.ThreadSafe;
36 import org.anarres.jdiagnostics.DefaultQuery;
37 import org.apache.commons.dbcp2.BasicDataSource;
38 import org.apache.commons.io.IOUtils;
39 import org.owasp.dependencycheck.utils.DBUtils;
40 import org.owasp.dependencycheck.utils.DependencyVersion;
41 import org.owasp.dependencycheck.utils.DependencyVersionUtil;
42 import org.owasp.dependencycheck.utils.FileUtils;
43 import org.owasp.dependencycheck.utils.Settings;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47
48
49
50
51
52
53
54
55 @ThreadSafe
56 public final class DatabaseManager {
57
58
59
60
61 private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseManager.class);
62
63
64
65 public static final String DB_STRUCTURE_RESOURCE = "data/initialize.sql";
66
67
68
69 public static final String DB_STRUCTURE_UPDATE_RESOURCE = "data/upgrade_%s.sql";
70
71
72
73 public static final String UPGRADE_HELP_URL = "https://jeremylong.github.io/DependencyCheck/data/upgrade.html";
74
75
76
77 private Driver driver = null;
78
79
80
81 private String connectionString = null;
82
83
84
85 private String userName = null;
86
87
88
89 private String password = null;
90
91
92
93
94 private int callDepth = 0;
95
96
97
98 private final Settings settings;
99
100
101
102 private boolean isH2;
103
104
105
106 private boolean isOracle;
107
108
109
110 private String databaseProductName;
111
112
113
114 private BasicDataSource connectionPool;
115
116
117
118
119
120
121
122
123 public DatabaseManager(Settings settings) throws DatabaseException {
124 this.settings = settings;
125 initialize();
126 }
127
128
129
130
131
132
133
134
135 private void initialize() throws DatabaseException {
136 final boolean autoUpdate = settings.getBoolean(Settings.KEYS.AUTO_UPDATE, true);
137 Connection conn = null;
138 try {
139
140 final String driverName = settings.getString(Settings.KEYS.DB_DRIVER_NAME, "");
141 if (!driverName.isEmpty()) {
142 final String driverPath = settings.getString(Settings.KEYS.DB_DRIVER_PATH, "");
143 LOGGER.debug("Loading driver '{}'", driverName);
144 try {
145 if (!driverPath.isEmpty()) {
146 LOGGER.debug("Loading driver from: {}", driverPath);
147 driver = DriverLoader.load(driverName, driverPath);
148 } else {
149 driver = DriverLoader.load(driverName);
150 }
151 } catch (DriverLoadException ex) {
152 LOGGER.debug("Unable to load database driver", ex);
153 throw new DatabaseException("Unable to load database driver", ex);
154 }
155 }
156 userName = settings.getString(Settings.KEYS.DB_USER, "dcuser");
157
158 password = settings.getString(Settings.KEYS.DB_PASSWORD, "DC-Pass1337!");
159 try {
160 connectionString = settings.getConnectionString(
161 Settings.KEYS.DB_CONNECTION_STRING,
162 Settings.KEYS.DB_FILE_NAME);
163 } catch (IOException ex) {
164 LOGGER.debug("Unable to retrieve the database connection string", ex);
165 throw new DatabaseException("Unable to retrieve the database connection string", ex);
166 }
167 isH2 = isH2Connection(connectionString);
168 boolean shouldCreateSchema = false;
169 try {
170 if (autoUpdate && isH2) {
171 shouldCreateSchema = !h2DataFileExists();
172 LOGGER.debug("Need to create DB Structure: {}", shouldCreateSchema);
173 }
174 } catch (IOException ioex) {
175 LOGGER.debug("Unable to verify database exists", ioex);
176 throw new DatabaseException("Unable to verify database exists", ioex);
177 }
178 LOGGER.debug("Loading database connection");
179 LOGGER.debug("Connection String: {}", connectionString);
180 LOGGER.debug("Database User: {}", userName);
181
182 try {
183 if (connectionString.toLowerCase().contains("integrated security=true")
184 || connectionString.toLowerCase().contains("trusted_connection=true")) {
185 conn = DriverManager.getConnection(connectionString);
186 } else {
187 conn = DriverManager.getConnection(connectionString, userName, password);
188 }
189 } catch (SQLException ex) {
190 if (ex.getMessage().contains("java.net.UnknownHostException") && connectionString.contains("AUTO_SERVER=TRUE;")) {
191 connectionString = connectionString.replace("AUTO_SERVER=TRUE;", "");
192 try {
193 conn = DriverManager.getConnection(connectionString, userName, password);
194 settings.setString(Settings.KEYS.DB_CONNECTION_STRING, connectionString);
195 LOGGER.debug("Unable to start the database in server mode; reverting to single user mode");
196 } catch (SQLException sqlex) {
197 LOGGER.debug("Unable to connect to the database", ex);
198 throw new DatabaseException("Unable to connect to the database", ex);
199 }
200 } else if (isH2 && ex.getMessage().contains("file version or invalid file header")) {
201 LOGGER.error("Incompatible or corrupt database found. To resolve this issue please remove the existing database by running purge");
202 throw new DatabaseException("Incompatible or corrupt database found; run the purge command to resolve the issue");
203 } else {
204 LOGGER.debug("Unable to connect to the database", ex);
205 throw new DatabaseException("Unable to connect to the database", ex);
206 }
207 }
208 databaseProductName = determineDatabaseProductName(conn);
209 isOracle = "oracle".equals(databaseProductName);
210 if (shouldCreateSchema) {
211 try {
212 createTables(conn);
213 } catch (DatabaseException dex) {
214 LOGGER.debug("", dex);
215 throw new DatabaseException("Unable to create the database structure", dex);
216 }
217 }
218 try {
219 ensureSchemaVersion(conn);
220 } catch (DatabaseException dex) {
221 LOGGER.debug("", dex);
222 throw new DatabaseException("Database schema does not match this version of dependency-check", dex);
223 }
224 } finally {
225 if (conn != null) {
226 try {
227 conn.close();
228 } catch (SQLException ex) {
229 LOGGER.debug("An error occurred closing the connection", ex);
230 }
231 }
232 }
233 }
234
235
236
237
238
239
240
241 private String determineDatabaseProductName(Connection conn) {
242 try {
243 final String databaseProductName = conn.getMetaData().getDatabaseProductName().toLowerCase();
244 LOGGER.debug("Database product: {}", databaseProductName);
245 return databaseProductName;
246 } catch (SQLException se) {
247 LOGGER.warn("Problem determining database product!", se);
248 return null;
249 }
250 }
251
252
253
254
255
256
257
258 public void cleanup() {
259 if (driver != null) {
260 DriverLoader.cleanup(driver);
261 driver = null;
262 }
263 connectionString = null;
264 userName = null;
265 password = null;
266 }
267
268
269
270
271
272
273
274
275
276 public boolean h2DataFileExists() throws IOException {
277 return h2DataFileExists(settings);
278 }
279
280
281
282
283
284
285
286
287
288
289 public static boolean h2DataFileExists(Settings configuration) throws IOException {
290 final File file = getH2DataFile(configuration);
291 return file.exists();
292 }
293
294
295
296
297
298
299
300
301 public static File getH2DataFile(Settings configuration) throws IOException {
302 final File dir = configuration.getH2DataDirectory();
303 final String fileName = configuration.getString(Settings.KEYS.DB_FILE_NAME);
304 return new File(dir, fileName);
305 }
306
307
308
309
310
311
312 public String getDatabaseProductName() {
313 return databaseProductName;
314 }
315
316
317
318
319
320
321 public boolean isH2Connection() {
322 return isH2;
323 }
324
325
326
327
328
329
330 public boolean isOracle() {
331 return isOracle;
332 }
333
334
335
336
337
338
339
340 public static boolean isH2Connection(Settings configuration) {
341 final String connStr;
342 try {
343 connStr = configuration.getConnectionString(
344 Settings.KEYS.DB_CONNECTION_STRING,
345 Settings.KEYS.DB_FILE_NAME);
346 } catch (IOException ex) {
347 LOGGER.debug("Unable to get connectionn string", ex);
348 return false;
349 }
350 return isH2Connection(connStr);
351 }
352
353
354
355
356
357
358
359 public static boolean isH2Connection(String connectionString) {
360 return connectionString.startsWith("jdbc:h2:file:");
361 }
362
363
364
365
366
367
368
369
370 private void createTables(Connection conn) throws DatabaseException {
371 LOGGER.debug("Creating database structure");
372 final String dbStructure;
373 try {
374 dbStructure = getResource(DB_STRUCTURE_RESOURCE);
375
376 Statement statement = null;
377 try {
378 statement = conn.createStatement();
379 statement.execute(dbStructure);
380 } catch (SQLException ex) {
381 LOGGER.debug("", ex);
382 throw new DatabaseException("Unable to create database statement", ex);
383 } finally {
384 DBUtils.closeStatement(statement);
385 }
386 } catch (IOException ex) {
387 throw new DatabaseException("Unable to create database schema", ex);
388 } catch (LinkageError ex) {
389 LOGGER.debug(new DefaultQuery(ex).call().toString());
390 }
391 }
392
393 private String getResource(String resource) throws IOException {
394 String dbStructure;
395 try {
396 final URL url = Resources.getResource(resource);
397 dbStructure = Resources.toString(url, StandardCharsets.UTF_8);
398 } catch (IllegalArgumentException ex) {
399 LOGGER.debug("Resources.getResource(String) failed to find the DB Structure Resource", ex);
400 try (InputStream is = FileUtils.getResourceAsStream(resource)) {
401 dbStructure = IOUtils.toString(is, StandardCharsets.UTF_8);
402 }
403 }
404 return dbStructure;
405 }
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420 private void updateSchema(Connection conn, DependencyVersion appExpectedVersion, DependencyVersion currentDbVersion)
421 throws DatabaseException {
422
423 if (connectionString.startsWith("jdbc:h2:file:")) {
424 LOGGER.debug("Updating database structure");
425 final String updateFile = String.format(DB_STRUCTURE_UPDATE_RESOURCE, currentDbVersion.toString());
426 if ("data/upgrade_4.2.sql".equals(updateFile) && !FileUtils.getResourceAsFile(updateFile).exists()) {
427 throw new DatabaseException("unable to upgrade the database schema - please run the dependency-check "
428 + "purge command to remove the existing database");
429 }
430 try {
431 final String dbStructureUpdate = getResource(updateFile);
432 Statement statement = null;
433 try {
434 statement = conn.createStatement();
435 statement.execute(dbStructureUpdate);
436 } catch (SQLException ex) {
437 throw new DatabaseException(String.format("Unable to upgrade the database schema from %s to %s",
438 currentDbVersion, appExpectedVersion.toString()), ex);
439 } finally {
440 DBUtils.closeStatement(statement);
441 }
442 } catch (IllegalArgumentException | IOException ex) {
443 final String msg = String.format("Upgrade SQL file does not exist: %s", updateFile);
444 throw new DatabaseException(msg, ex);
445 }
446 } else {
447 final int e0 = Integer.parseInt(appExpectedVersion.getVersionParts().get(0));
448 final int c0 = Integer.parseInt(currentDbVersion.getVersionParts().get(0));
449 final int e1 = Integer.parseInt(appExpectedVersion.getVersionParts().get(1));
450 final int c1 = Integer.parseInt(currentDbVersion.getVersionParts().get(1));
451
452 if (e0 == c0 && e1 < c1) {
453 LOGGER.warn("A new version of dependency-check is available; consider upgrading");
454 settings.setBoolean(Settings.KEYS.AUTO_UPDATE, false);
455 } else if (e0 == c0 && e1 == c1) {
456
457 } else {
458 LOGGER.error("The database schema must be upgraded to use this version of dependency-check. Please see {} for more information.",
459 UPGRADE_HELP_URL);
460 throw new DatabaseException("Database schema is out of date");
461 }
462
463 }
464 }
465
466
467
468
469
470
471
472 public ResourceBundle getSqlStatements() {
473 final ResourceBundle statementBundle = getDatabaseProductName() != null
474 ? ResourceBundle.getBundle("data/dbStatements", new Locale(getDatabaseProductName()))
475 : ResourceBundle.getBundle("data/dbStatements");
476 return statementBundle;
477 }
478
479
480
481
482
483
484
485
486
487 private void ensureSchemaVersion(Connection conn) throws DatabaseException {
488 ResultSet rs = null;
489 PreparedStatement ps = null;
490 final ResourceBundle statementBundle = getSqlStatements();
491 final String sql = statementBundle.getString("SELECT_SCHEMA_VERSION");
492 try {
493 ps = conn.prepareStatement(sql);
494 rs = ps.executeQuery();
495 if (rs.next()) {
496 final String dbSchemaVersion = settings.getString(Settings.KEYS.DB_VERSION);
497 final DependencyVersion appDbVersion = DependencyVersionUtil.parseVersion(dbSchemaVersion);
498 if (appDbVersion == null) {
499 throw new DatabaseException("Invalid application database schema");
500 }
501 final DependencyVersion db = DependencyVersionUtil.parseVersion(rs.getString(1));
502 if (db == null) {
503 throw new DatabaseException("Invalid database schema");
504 }
505 LOGGER.debug("DC Schema: {}", appDbVersion);
506 LOGGER.debug("DB Schema: {}", db);
507 if (appDbVersion.compareTo(db) > 0) {
508 final boolean autoUpdate = settings.getBoolean(Settings.KEYS.AUTO_UPDATE, true);
509 if (autoUpdate) {
510 updateSchema(conn, appDbVersion, db);
511 if (++callDepth < 10) {
512 ensureSchemaVersion(conn);
513 }
514 } else {
515 throw new DatabaseException("Old database schema identified - please execute "
516 + "dependency-check without the no-update configuration to continue");
517 }
518 }
519 } else {
520 throw new DatabaseException("Database schema is missing");
521 }
522 } catch (SQLException ex) {
523 LOGGER.debug("", ex);
524 throw new DatabaseException("Unable to check the database schema version", ex);
525 } finally {
526 DBUtils.closeResultSet(rs);
527 DBUtils.closeStatement(ps);
528 }
529 }
530
531
532
533
534 public void open() {
535 connectionPool = new BasicDataSource();
536 if (driver != null) {
537 connectionPool.setDriver(driver);
538 }
539 connectionPool.setUrl(connectionString);
540 connectionPool.setUsername(userName);
541 connectionPool.setPassword(password);
542 }
543
544
545
546
547 public void close() {
548 try {
549 connectionPool.close();
550 } catch (SQLException ex) {
551 LOGGER.debug("Error closing the connection pool", ex);
552 }
553 connectionPool = null;
554 }
555
556
557
558
559
560
561 public boolean isOpen() {
562 return connectionPool != null;
563 }
564
565
566
567
568
569
570
571
572
573 public Connection getConnection() throws DatabaseException {
574 try {
575 return connectionPool.getConnection();
576 } catch (SQLException ex) {
577 throw new DatabaseException("Error connecting to the database", ex);
578 }
579 }
580 }