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 LOGGER.warn("Explicitly loaded driver {} from classpath; if JDBCv4 service loading is supported "
151 + "by the driver you should remove the dbDriver configuration", driverName);
152 }
153 } catch (DriverLoadException ex) {
154 LOGGER.debug("Unable to load database driver", ex);
155 throw new DatabaseException("Unable to load database driver", ex);
156 }
157 }
158 userName = settings.getString(Settings.KEYS.DB_USER, "dcuser");
159
160 password = settings.getString(Settings.KEYS.DB_PASSWORD, "DC-Pass1337!");
161 try {
162 connectionString = settings.getConnectionString(
163 Settings.KEYS.DB_CONNECTION_STRING,
164 Settings.KEYS.DB_FILE_NAME);
165 } catch (IOException ex) {
166 LOGGER.debug("Unable to retrieve the database connection string", ex);
167 throw new DatabaseException("Unable to retrieve the database connection string", ex);
168 }
169 isH2 = isH2Connection(connectionString);
170 boolean shouldCreateSchema = false;
171 try {
172 if (autoUpdate && isH2) {
173 shouldCreateSchema = !h2DataFileExists();
174 LOGGER.debug("Need to create DB Structure: {}", shouldCreateSchema);
175 }
176 } catch (IOException ioex) {
177 LOGGER.debug("Unable to verify database exists", ioex);
178 throw new DatabaseException("Unable to verify database exists", ioex);
179 }
180 LOGGER.debug("Loading database connection");
181 LOGGER.debug("Connection String: {}", connectionString);
182 LOGGER.debug("Database User: {}", userName);
183
184 try {
185 if (connectionString.toLowerCase().contains("integrated security=true")
186 || connectionString.toLowerCase().contains("trusted_connection=true")) {
187 conn = DriverManager.getConnection(connectionString);
188 } else {
189 conn = DriverManager.getConnection(connectionString, userName, password);
190 }
191 } catch (SQLException ex) {
192 if (ex.getMessage().contains("java.net.UnknownHostException") && connectionString.contains("AUTO_SERVER=TRUE;")) {
193 connectionString = connectionString.replace("AUTO_SERVER=TRUE;", "");
194 try {
195 conn = DriverManager.getConnection(connectionString, userName, password);
196 settings.setString(Settings.KEYS.DB_CONNECTION_STRING, connectionString);
197 LOGGER.debug("Unable to start the database in server mode; reverting to single user mode");
198 } catch (SQLException sqlex) {
199 LOGGER.debug("Unable to connect to the database", ex);
200 throw new DatabaseException("Unable to connect to the database", ex);
201 }
202 } else if (isH2 && ex.getMessage().contains("file version or invalid file header")) {
203 LOGGER.error("Incompatible or corrupt database found. To resolve this issue please remove the existing "
204 + "database by running purge");
205 throw new DatabaseException("Incompatible or corrupt database found; run the purge command to resolve the issue");
206 } else {
207 LOGGER.debug("Unable to connect to the database", ex);
208 throw new DatabaseException("Unable to connect to the database", ex);
209 }
210 }
211 databaseProductName = determineDatabaseProductName(conn);
212 isOracle = "oracle".equals(databaseProductName);
213 if (shouldCreateSchema) {
214 try {
215 createTables(conn);
216 } catch (DatabaseException dex) {
217 LOGGER.debug("", dex);
218 throw new DatabaseException("Unable to create the database structure", dex);
219 }
220 }
221 try {
222 ensureSchemaVersion(conn);
223 } catch (DatabaseException dex) {
224 LOGGER.debug("", dex);
225 throw new DatabaseException("Database schema does not match this version of dependency-check", dex);
226 }
227 } finally {
228 if (conn != null) {
229 try {
230 conn.close();
231 } catch (SQLException ex) {
232 LOGGER.debug("An error occurred closing the connection", ex);
233 }
234 }
235 }
236 }
237
238
239
240
241
242
243
244 private String determineDatabaseProductName(Connection conn) {
245 try {
246 final String databaseProductName = conn.getMetaData().getDatabaseProductName().toLowerCase();
247 LOGGER.debug("Database product: {}", databaseProductName);
248 return databaseProductName;
249 } catch (SQLException se) {
250 LOGGER.warn("Problem determining database product!", se);
251 return null;
252 }
253 }
254
255
256
257
258
259
260
261 public void cleanup() {
262 if (driver != null) {
263 DriverLoader.cleanup(driver);
264 driver = null;
265 }
266 connectionString = null;
267 userName = null;
268 password = null;
269 }
270
271
272
273
274
275
276
277
278
279 public boolean h2DataFileExists() throws IOException {
280 return h2DataFileExists(settings);
281 }
282
283
284
285
286
287
288
289
290
291
292 public static boolean h2DataFileExists(Settings configuration) throws IOException {
293 final File file = getH2DataFile(configuration);
294 return file.exists();
295 }
296
297
298
299
300
301
302
303
304 public static File getH2DataFile(Settings configuration) throws IOException {
305 final File dir = configuration.getH2DataDirectory();
306 final String fileName = configuration.getString(Settings.KEYS.DB_FILE_NAME);
307 return new File(dir, fileName);
308 }
309
310
311
312
313
314
315 public String getDatabaseProductName() {
316 return databaseProductName;
317 }
318
319
320
321
322
323
324 public boolean isH2Connection() {
325 return isH2;
326 }
327
328
329
330
331
332
333 public boolean isOracle() {
334 return isOracle;
335 }
336
337
338
339
340
341
342
343 public static boolean isH2Connection(Settings configuration) {
344 final String connStr;
345 try {
346 connStr = configuration.getConnectionString(
347 Settings.KEYS.DB_CONNECTION_STRING,
348 Settings.KEYS.DB_FILE_NAME);
349 } catch (IOException ex) {
350 LOGGER.debug("Unable to get connectionn string", ex);
351 return false;
352 }
353 return isH2Connection(connStr);
354 }
355
356
357
358
359
360
361
362 public static boolean isH2Connection(String connectionString) {
363 return connectionString.startsWith("jdbc:h2:file:");
364 }
365
366
367
368
369
370
371
372
373 private void createTables(Connection conn) throws DatabaseException {
374 LOGGER.debug("Creating database structure");
375 final String dbStructure;
376 try {
377 dbStructure = getResource(DB_STRUCTURE_RESOURCE);
378
379 Statement statement = null;
380 try {
381 statement = conn.createStatement();
382 statement.execute(dbStructure);
383 } catch (SQLException ex) {
384 LOGGER.debug("", ex);
385 throw new DatabaseException("Unable to create database statement", ex);
386 } finally {
387 DBUtils.closeStatement(statement);
388 }
389 } catch (IOException ex) {
390 throw new DatabaseException("Unable to create database schema", ex);
391 } catch (LinkageError ex) {
392 LOGGER.debug(new DefaultQuery(ex).call().toString());
393 }
394 }
395
396 private String getResource(String resource) throws IOException {
397 String dbStructure;
398 try {
399 final URL url = Resources.getResource(resource);
400 dbStructure = Resources.toString(url, StandardCharsets.UTF_8);
401 } catch (IllegalArgumentException ex) {
402 LOGGER.debug("Resources.getResource(String) failed to find the DB Structure Resource", ex);
403 try (InputStream is = FileUtils.getResourceAsStream(resource)) {
404 dbStructure = IOUtils.toString(is, StandardCharsets.UTF_8);
405 }
406 }
407 return dbStructure;
408 }
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423 private void updateSchema(Connection conn, DependencyVersion appExpectedVersion, DependencyVersion currentDbVersion)
424 throws DatabaseException {
425
426 if (connectionString.startsWith("jdbc:h2:file:")) {
427 LOGGER.debug("Updating database structure");
428 final String updateFile = String.format(DB_STRUCTURE_UPDATE_RESOURCE, currentDbVersion.toString());
429 if ("data/upgrade_4.2.sql".equals(updateFile) && !FileUtils.getResourceAsFile(updateFile).exists()) {
430 throw new DatabaseException("unable to upgrade the database schema - please run the dependency-check "
431 + "purge command to remove the existing database");
432 }
433 try {
434 final String dbStructureUpdate = getResource(updateFile);
435 Statement statement = null;
436 try {
437 statement = conn.createStatement();
438 statement.execute(dbStructureUpdate);
439 } catch (SQLException ex) {
440 throw new DatabaseException(String.format("Unable to upgrade the database schema from %s to %s",
441 currentDbVersion, appExpectedVersion.toString()), ex);
442 } finally {
443 DBUtils.closeStatement(statement);
444 }
445 } catch (IllegalArgumentException | IOException ex) {
446 final String msg = String.format("Upgrade SQL file does not exist: %s", updateFile);
447 throw new DatabaseException(msg, ex);
448 }
449 } else {
450 final int e0 = Integer.parseInt(appExpectedVersion.getVersionParts().get(0));
451 final int c0 = Integer.parseInt(currentDbVersion.getVersionParts().get(0));
452 final int e1 = Integer.parseInt(appExpectedVersion.getVersionParts().get(1));
453 final int c1 = Integer.parseInt(currentDbVersion.getVersionParts().get(1));
454
455 if (e0 == c0 && e1 < c1) {
456 LOGGER.warn("A new version of dependency-check is available; consider upgrading");
457 settings.setBoolean(Settings.KEYS.AUTO_UPDATE, false);
458 } else if (e0 == c0 && e1 == c1) {
459
460 } else {
461 LOGGER.error("The database schema must be upgraded to use this version of dependency-check. Please see {} for more information.",
462 UPGRADE_HELP_URL);
463 throw new DatabaseException("Database schema is out of date");
464 }
465
466 }
467 }
468
469
470
471
472
473
474
475 public ResourceBundle getSqlStatements() {
476 final ResourceBundle statementBundle = getDatabaseProductName() != null
477 ? ResourceBundle.getBundle("data/dbStatements", new Locale(getDatabaseProductName()))
478 : ResourceBundle.getBundle("data/dbStatements");
479 return statementBundle;
480 }
481
482
483
484
485
486
487
488
489
490 private void ensureSchemaVersion(Connection conn) throws DatabaseException {
491 ResultSet rs = null;
492 PreparedStatement ps = null;
493 final ResourceBundle statementBundle = getSqlStatements();
494 final String sql = statementBundle.getString("SELECT_SCHEMA_VERSION");
495 try {
496 ps = conn.prepareStatement(sql);
497 rs = ps.executeQuery();
498 if (rs.next()) {
499 final String dbSchemaVersion = settings.getString(Settings.KEYS.DB_VERSION);
500 final DependencyVersion appDbVersion = DependencyVersionUtil.parseVersion(dbSchemaVersion);
501 if (appDbVersion == null) {
502 throw new DatabaseException("Invalid application database schema");
503 }
504 final DependencyVersion db = DependencyVersionUtil.parseVersion(rs.getString(1));
505 if (db == null) {
506 throw new DatabaseException("Invalid database schema");
507 }
508 LOGGER.debug("DC Schema: {}", appDbVersion);
509 LOGGER.debug("DB Schema: {}", db);
510 if (appDbVersion.compareTo(db) > 0) {
511 final boolean autoUpdate = settings.getBoolean(Settings.KEYS.AUTO_UPDATE, true);
512 if (autoUpdate) {
513 updateSchema(conn, appDbVersion, db);
514 if (++callDepth < 10) {
515 ensureSchemaVersion(conn);
516 }
517 } else {
518 throw new DatabaseException("Old database schema identified - please execute "
519 + "dependency-check without the no-update configuration to continue");
520 }
521 }
522 } else {
523 throw new DatabaseException("Database schema is missing");
524 }
525 } catch (SQLException ex) {
526 LOGGER.debug("", ex);
527 throw new DatabaseException("Unable to check the database schema version", ex);
528 } finally {
529 DBUtils.closeResultSet(rs);
530 DBUtils.closeStatement(ps);
531 }
532 }
533
534
535
536
537 public void open() {
538 connectionPool = new BasicDataSource();
539 if (driver != null) {
540 connectionPool.setDriver(driver);
541 }
542 connectionPool.setUrl(connectionString);
543 connectionPool.setUsername(userName);
544 connectionPool.setPassword(password);
545 }
546
547
548
549
550 public void close() {
551 try {
552 connectionPool.close();
553 } catch (SQLException ex) {
554 LOGGER.debug("Error closing the connection pool", ex);
555 }
556 connectionPool = null;
557 }
558
559
560
561
562
563
564 public boolean isOpen() {
565 return connectionPool != null;
566 }
567
568
569
570
571
572
573
574
575
576 public Connection getConnection() throws DatabaseException {
577 try {
578 return connectionPool.getConnection();
579 } catch (SQLException ex) {
580 throw new DatabaseException("Error connecting to the database", ex);
581 }
582 }
583 }